在上一篇,我們完成交友任務管理的API雛形開發。
在本篇,我們要"玩真的",將資料庫(Azure Table Storage)串接到API上!

在進行開發之前,我們先來快速地介紹一下Azure Table Storage!
他是表格式的儲存體,適合較為結構化的資料,例如會員資料,圖書館書籍資料,購物車。
大家可能會聯想到SQL Server,但實際上它不是關聯式資料庫RDBMS)。比較類似Redis的Key-Value儲存。
如果你有時可能會彈性的加欄位,那Azure Table Storage是你很好的選擇!


接下來我們直接進入正題,準備來串接Azure Table Storage來開發程式囉!
完整程式碼下載:https://github.com/hatsukiotowa/AzureFunctionsRestAPI_TableStorage



完成後,我們無法直接在Portal上新增資料,必須使用Microsoft Azure Storage Explorer,或是Visual Studio 2017的Cloud Explorer進行操作。


這邊快速的介紹一下,Partition Key和Row Key:
在Key-Value儲存的世界中,Partition Key有點像資料放置的位置,是一個Hash
而Row Key和Partition一同搭配使用,用來作為做排序的依據

針對Azure Table Storage的Entity與ToDo進行轉換
public static class Mappings
{
    public static TodoTableEntity ToTableEntity(this Todo todo)
    {
        return new TodoTableEntity()
        {
            PartitionKey = "TODO",
            RowKey = todo.Id,
            CreatedTime = todo.CreatedTime,
            IsCompleted = todo.IsCompleted,
            MissionDescription = todo.MissionDescription
        };
    }
    public static Todo ToTodo(this TodoTableEntity todo)
    {
        return new Todo()
        {
            Id = todo.RowKey,
            CreatedTime = todo.CreatedTime,
            IsCompleted = todo.IsCompleted,
            MissionDescription = todo.MissionDescription
        };
    }
}
相關資料可以在Account Storage的Access Keys查看
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName={YourAccountName};AccountKey={YourAccountKey};BlobEndpoint=https://{YourAccountName}.blob.core.windows.net/;TableEndpoint=https://{YourAccountName}.table.core.windows.net/;QueueEndpoint=https://{YourAccountName}.queue.core.windows.net/;FileEndpoint=https://{YourAccountName}.file.core.windows.net/",
    "AzureWebJobsDashboard": "DefaultEndpointsProtocol=https;AccountName={YourAccountName};AccountKey={YourAccountKey};BlobEndpoint=https://{YourAccountName}.blob.core.windows.net/;TableEndpoint=https://{YourAccountName}.table.core.windows.net/;QueueEndpoint=https://{YourAccountName}.queue.core.windows.net/;FileEndpoint=https://{YourAccountName}.file.core.windows.net/",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "Host": {
    "CORS": "*"
  }
}
完整程式碼:
[FunctionName("CreateTodo")]
public static async Task<IActionResult> CreateTodo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "missions")]HttpRequest req, 
    [Table("Todos", Connection = "AzureWebJobsStorage" )] IAsyncCollector<TodoTableEntity> todoTable,
    TraceWriter log)
{
    log.Info("Creating a new todo list item");
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    var input = JsonConvert.DeserializeObject<TodoCreateModel>(requestBody);
    var todo = new Todo() { MissionDescription = input.MissionDescription };
    await todoTable.AddAsync(todo.ToTableEntity());
    return new OkObjectResult(todo);
}
在修改的程式碼裡,我們在參數增加下行,串接Azure Table Storage,Connection裡的AzureWebJobsStorage,即是我們在local.settings.json定義的連線資訊
 [Table("Todos", Connection = "AzureWebJobsStorage" )] IAsyncCollector<TodoTableEntity> todoTable,
我們將todo的item轉換成Table Storage能讀取的TableEntity
await todoTable.AddAsync(todo.ToTableEntity());
完整程式碼:
[FunctionName("GetTodos")]
public static async Task<IActionResult> GetTodos(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "missions")]HttpRequest req,
    [Table("Todos", Connection = "AzureWebJobsStorage")] CloudTable todoTable,
    TraceWriter log)
{
    log.Info("Getting todo list items");
    var query = new TableQuery<TodoTableEntity>();
    var segment = await todoTable.ExecuteQuerySegmentedAsync(query, null);
    return new OkObjectResult(segment.Select(Mappings.ToTodo));
}
CloudTable提供我們Query Table的功能
[Table("Todos", Connection = "AzureWebJobsStorage")] CloudTable todoTable
我們Query Table內的所有資料
var query = new TableQuery<TodoTableEntity>();
var segment = await todoTable.ExecuteQuerySegmentedAsync(query, null);
回傳轉成Todo的Objects
return new OkObjectResult(segment.Select(Mappings.ToTodo));
[FunctionName("GetTodoById")]
public static async Task<IActionResult> GetTodoById(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "mission/{id}")]HttpRequest req,
    [Table("Todos", "TODO", "{id}", Connection = "AzureWebJobsStorage")] TodoTableEntity todo,
    TraceWriter log, string id)
{
    log.Info("Getting todo item by id");
    if (todo == null)
    {
        log.Info($"Item {id} not found");
        return new NotFoundResult();
    }
    return new OkObjectResult(todo.ToTodo());
}
Table的第一個參數是Table Name, TODO是我們指定的Partition Key, {id}是Row key
 [Table("Todos", "TODO", "{id}", Connection = "AzureWebJobsStorage")] TodoTableEntity todo,
完整程式碼:
[FunctionName("UpdateTodo")]
public static async Task<IActionResult> UpdateTodo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "put", Route = "mission/{id}")]HttpRequest req,
    [Table("Todos", Connection = "AzureWebJobsStorage")] CloudTable todoTable,
    TraceWriter log, string id)
{
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    var updated = JsonConvert.DeserializeObject<TodoUpdateModel>(requestBody);
    var findOperation = TableOperation.Retrieve<TodoTableEntity>("TODO", id);
    var findResult = await todoTable.ExecuteAsync(findOperation);
    if (findResult.Result == null)
    {
        return new NotFoundResult();
    }
    var existingRow = (TodoTableEntity)findResult.Result;
    existingRow.IsCompleted = updated.IsCompleted;
    if (!string.IsNullOrEmpty(updated.MissionDescription))
    {
        existingRow.MissionDescription = updated.MissionDescription;
    }
    var replaceOperation = TableOperation.Replace(existingRow);
    await todoTable.ExecuteAsync(replaceOperation);
    return new OkObjectResult(existingRow.ToTodo());
}
使用ID找到對應的item進行修改
var findOperation = TableOperation.Retrieve<TodoTableEntity>("TODO", id);
var findResult = await todoTable.ExecuteAsync(findOperation);
if (findResult.Result == null)
{
    return new NotFoundResult();
}
修改並覆蓋已有的資料
var existingRow = (TodoTableEntity)findResult.Result;
existingRow.IsCompleted = updated.IsCompleted;
if (!string.IsNullOrEmpty(updated.MissionDescription))
{
    existingRow.MissionDescription = updated.MissionDescription;
}
var replaceOperation = TableOperation.Replace(existingRow);
await todoTable.ExecuteAsync(replaceOperation);
完整程式碼:
[FunctionName("DeleteTodo")]
public static async Task<IActionResult> DeleteTodo(
    [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "mission/{id}")]HttpRequest req,
    [Table("Todos", Connection = "AzureWebJobsStorage")] CloudTable todoTable,
    TraceWriter log, string id)
{
    var deleteOperation = TableOperation.Delete(new TableEntity()
    { PartitionKey = "TODO", RowKey = id, ETag = "*" });
    try
    {
        var deleteResult = await todoTable.ExecuteAsync(deleteOperation);
    }catch(StorageException e) when (e.RequestInformation.HttpStatusCode == 404)
    {
        return new NotFoundResult();
    }
    return new OkResult();
}
這邊比較特別的是etag,是針對Concurrency進行優化。
設定成ETag = "*" 之後,實作"last-write-wins"的機制
var deleteOperation = TableOperation.Delete(new TableEntity()
    { PartitionKey = "TODO", RowKey = id, ETag = "*" });

完整程式碼下載:https://github.com/hatsukiotowa/AzureFunctionsRestAPI_TableStorage
完成Table Storage的實作後,大家會不會也想了解其他的儲存方式呢?
在下一篇我們將介紹Azure Queue Storage,是另一種儲存形式,快速連結在此:Azure Queue Storage介紹