[料理佳餚] Azure Storage 除了能儲存檔案之外,還能化身為資料表(Table)儲存結構化資料。

Azure Table Storage 是 Azure Storage 中的一個服務,可以讓我們存放結構化的 NoSQL 資料,而且可以存放到 PB(Petabyte)級以上的資料量,價格也是很親民,依據備援策略的不同,最便宜可以到每 GB $0.045 鎂一個月,本篇文章用 CRUD 的範例來簡單介紹一下 Azure Table Storage。

建立儲存體帳戶

首先,我們需要建立一個儲存體帳戶,建立的步驟就請參考之前文章中「建立儲存體帳戶」的部分,這邊就不再贅述。

建立資料表

儲存體帳戶建好後,我們到後台利用「儲存體瀏覽器」來建立資料表,切換到「資料表」之後,點擊「+新增資料表」,輸入「資料表名稱」後,點擊「確定」將資料表建起來。

資料表建好後,預設系統會建立三個欄位,分別是:PartitionKeyRowKeyTimestamp,其中 Timestamp 由系統自動管理,我們不需要處理它,而 PartitionKey 及 RowKey 則是組合成為一筆資料的主索引鍵。

其中 PartitionKey 尤為重要,Azure 會視情況將資料表內的資料進行分割,分散儲存在不同的伺服器上,而 PartitionKey 即是資料分割的依據,PartitionKey 的值設計得太粗或太細都不好,太粗會導致資料分割太大,資料儲存太過於集中在某一台伺服器上;太細,則是會讓資料分割太小,查詢資料時跨越太多的伺服器,因此 PartitionKey 的設計得依據應用程式的需求,慎重思考。

起手式

我打算使用 C# 來存取 Azure Table Storage,我們將專案建好之後,接著安裝 Azure.Data.Tables 套件,然後在後台「存取金鑰」的畫面中,複製其中一組「連線字串」,待會兒會用到。

新增資料

在 Azure Table Storage 一筆資料叫一個「Entity」,新增一筆資料就是新增一個 Entity,雖然 Azure.Data.Tables 套件有提供 TableEntity 這個類別讓我們可以使用,但是如果可以的話,我還是比較建議在我們的資料模型上去實作 ITableEntity 這個介面,在資料的操作上會方便很多。

在範例中我建立了一個 MemberEntity 類別,設計上就直接指定 Department 為 PartitionKey,Id 為 RowKey。

public class MemberEntity : ITableEntity
{
    private string partitionKey;
    private string rowKey;

    public MemberEntity()
    {
    }

    public MemberEntity(string department, int id)
    {
        this.Department = department;
        this.Id = id;
    }

    public string PartitionKey
    {
        get => this.partitionKey ??= this.Department;
        set => this.partitionKey = value;
    }

    public string RowKey
    {
        get => this.rowKey ??= this.Id.ToString();
        set => this.rowKey = value;
    }

    public DateTimeOffset? Timestamp { get; set; }

    public ETag ETag { get; set; }

    // ==User Defined Properties==
    
    public int Id { get; set; }

    public string Name { get; set; }

    public string Department { get; set; }

    public string City { get; set; }

    public long ModifiedCount { get; set; }
    
    // ==User Defined Properties==
}

新增一個 Entity 的程式碼如下,我們先產生 TableServiceClient,然後呼叫 GetTableClient() 方法取得 TableClient,最後呼叫 AddEntity() / AddEntityAsync() 方法,將 Entity 寫入到 Azure Table Storage,而且我們不必預先定義欄位,缺少的欄位定義會自動補上。

private static Task Create()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    var memberEntity = new MemberEntity("總管理處", 1) { Name = "Johnny", City = "Taipei", ModifiedCount = 1 };

    return tableClient.AddEntityAsync(memberEntity);
}

查詢資料

要從 Azure Table Storage 撈資料出來,如果我們知道主索引鍵(PartitionKey + RowKey)的話,可以直接呼叫 GetEntity<T>() / GetEntityAsync<T>() 方法就可以將資料抓出來,其中 select 參數可以讓我們指定回傳部分欄位。

private static async Task QueryByKey()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    var response = await tableClient.GetEntityAsync<MemberEntity>("總管理處", "1");

    Console.WriteLine(response.Value.Name);

    // 回傳部分欄位
    response = await tableClient.GetEntityAsync<MemberEntity>("總管理處", "1", new[] { nameof(MemberEntity.City) });

    Console.WriteLine(response.Value.City);
}

如果我們不知道資料的主索引鍵為何,我們可以改用 Query<T>() / QueryAsync<T>() 方法,撰寫 filter 查詢語法給 Azure Table Storage 去查詢資料,filter 查詢的語法可以在「查詢資料表和實體」這份官方文件中找到,而我比較傾向使用另一個多載方法,改用 Expression 來寫 filter 查詢語法。

private static async Task QueryByFilter()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    var response = tableClient.QueryAsync<MemberEntity>(x => x.PartitionKey == "總管理處" && x.City == "Taipei");
    
    await foreach (var page in response.AsPages())
    {
        foreach (var memberEntity in page.Values)
        {
            Console.WriteLine(memberEntity.Name);
        }
    }
}

用 filter 查詢語法來查詢資料有效能上的問題要注意一下,查詢語法內包不包含 PartitionKey、RowKey,會影響查詢的策略,影響的範圍如下表:

包含 PartitionKey包含 RowKey查詢策略效能
Row Scan最好;但有可能因為資料分割龐大而發生錯誤。
Partition Scan中等;觸及多個資料分割伺服器,有可能觸及太多伺服器而發生延遲。
Table Scan最差;觸及全部的資料分割。

更新資料

要更新既存資料可以使用 UpdateEntity() / UpdateEntityAsync() 方法,將整個 Entity 傳進去,如果要更新部分欄位,則可以改傳入 TableEntity,並指定要更新的欄位,第二個參數則暫時先指定 ETag.All 即可,後面說明。

private static async Task Update()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    var queryResponse = await tableClient.GetEntityAsync<MemberEntity>("總管理處", "1");

    var memberEntity = queryResponse.Value;

    memberEntity.City = "New Taipei";

    _ = await tableClient.UpdateEntityAsync(memberEntity, ETag.All);

    // 更新部分欄位
    _ = await tableClient.UpdateEntityAsync(new TableEntity("總管理處", "1") { { "City", "New Taipei" } }, ETag.All);
}

更新方法的第二個參數 ifMatch,如果我們有指定值,則在更新的時候,Azure Table Storage 會去跟既存資料的 ETag 值做比對,相同才執行更新,不同則回傳 412 (Precondition Failed) 訊息,可以讓我們用來避免 Dirty Write(髒寫)

更新方法的第三個參數 mode,代表更新的方式,預設值是 TableUpdateMode.Merge,多的欄位會新增、少的欄位不刪除,如果我們有需要可以調整成 TableUpdateMode.Replace(取代更新),整個 Entity 會用被更新的資料取代掉。

private static async Task ReplaceUpdate()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    // 取代更新
    _ = await tableClient.UpdateEntityAsync(new TableEntity("總管理處", "1") { { "Name", "Tom" } }, ETag.All, TableUpdateMode.Replace);
}

更新或新增資料

除了單純的新增/更新資料的 API 之外,還有一個好用的更新或新增 - UpsertEntity() / UpsertEntityAsync() 方法,資料存在就執行更新,資料不存在執行新增。

private static async Task Upsert()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    var memberEntity = new MemberEntity("總管理處", 1) { Name = "Johnny", City = "Taipei", ModifiedCount = 1 };

    _ = await tableClient.UpsertEntityAsync(memberEntity);

}

刪除資料

最後是刪除資料的 API - DeleteEntity() / DeleteEntityAsync(),刪除的方法就比較單純,只能使用主索引鍵去把資料找出來刪除。

private static async Task Delete()
{
    var tableServiceClient = new TableServiceClient(ConnectionString);

    var tableClient = tableServiceClient.GetTableClient("MyTable");

    _ = await tableClient.DeleteEntityAsync("總管理處", "1");
}

我們一份資料要儲存在哪裡?儲存的格式為何?其實都要評估當下的情境跟需求而定,如果我們只會一招「開機器,丟 SQL。」,不僅錯失了其他可能更合適的解決方案,成本上的拿捏上也失去了一些彈性,Azure Table Storage 只是市面上眾多資料儲存解決方案的其中一種,多了解、多掌握,手上也就多一項武器。

參考資料

 < Source Code >

相關資源

C# 指南
ASP.NET 教學
ASP.NET MVC 指引
Azure SQL Database 教學
SQL Server 教學
Xamarin.Forms 教學