Azure Table Storage 是 Azure Storage 中的一個服務,可以讓我們存放結構化的 NoSQL 資料,而且可以存放到 PB(Petabyte)級以上的資料量,價格也是很親民,依據備援策略的不同,最便宜可以到每 GB $0.045 鎂一個月,本篇文章用 CRUD 的範例來簡單介紹一下 Azure Table Storage。
建立儲存體帳戶
首先,我們需要建立一個儲存體帳戶,建立的步驟就請參考之前文章中「建立儲存體帳戶」的部分,這邊就不再贅述。
建立資料表
儲存體帳戶建好後,我們到後台利用「儲存體瀏覽器
」來建立資料表,切換到「資料表
」之後,點擊「+新增資料表
」,輸入「資料表名稱
」後,點擊「確定
」將資料表建起來。
資料表建好後,預設系統會建立三個欄位,分別是:PartitionKey
、RowKey
、Timestamp
,其中 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 >