Azure Table Storage API 泛型設計

Azure Table Storage API 泛型設計

最近有機會使用到Azure Table Storage服務,Table Storage是早期Azure儲存體一種NoSQL儲存方式,程式開發上花點時間看看官網的文件,跟著範例做一下,大致上不會有太大問題。不過等真的要使用時會發現,如果按照範例程式的寫法,基本上是一整個寫死,沒有彈性可言。

建立Entity(實體)

Table Storage 的 SDK 設計是你在呼叫 Table API 之前必須傳入要儲存的 Entity(實體),如果讀者熟悉 Entity Framework 的話,可以想成 Entity Framework 的 Entity,如官網範例的 CustomerEntity

 public class CustomerEntity : TableEntity
 {
     public CustomerEntity(string lastName, string firstName)
     {
         this.PartitionKey = lastName;
         this.RowKey = firstName;
     }

     public CustomerEntity() { }
     public string Email { get; set; }
     public string PhoneNumber { get; set; }
 }  
 

然後進行CRUD操作時都是以 Entity 為單位,例如查詢:

 // Create a new customer entity.
 CustomerEntity customer1 = new CustomerEntity("Harp", "Walter");
 customer1.Email = "Walter@contoso.com";
 customer1.PhoneNumber = "425-555-0101";

 // Create the TableOperation object that inserts the customer entity.
 TableOperation insertOperation = TableOperation.Insert(customer1);

 // Execute the insert operation.
 table.Execute(insertOperation);  
 

在正式使用碰到的問題就是,我們通常針對這類操作的 API 會先抽離出來給大家共用,但問題是,此 Table Storage API 並不會事前知道你所要操作的 Entity 格式,Entity 格式應是留給使用端去定義,這樣才能發揮 NoSQL 的長處,如果一開始定義寫死了,那就和關連式資料庫沒有差別了。

針對上述問題,我參考官網的範例寫了個泛型版本的 Table Storage API 版本,透過泛型就能輕鬆解決 Entity 型別的問題。

    /// <summary>
    /// Table Storage
    /// </summary>
    public class TableStorage<T> where T : TableEntity, new()
    {
        private string tableConn;
        private string tableName;
        private CloudTable table;

        /// <summary>
        /// 初始化 Table 物件。
        /// </summary>
        /// <param name="tableConn">連線字串</param>
        /// <param name="tableName">存取表格名稱</param>
        public TableStorage(string tableConn, string tableName)
        {
            this.tableConn = tableConn;
            this.tableName = tableName;

            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(tableConn);
            CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
            table = tableClient.GetTableReference(tableName);
            table.CreateIfNotExists();
        }

        /// <summary>
        /// 新增資料至Table儲存區。
        /// 注意,不能存放重覆key值資料。
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public string Insert(T entity)
        {
            TableOperation insertOperation = TableOperation.Insert(entity);
            try
            {
                table.Execute(insertOperation);
            }
            catch (Exception ex)
            {
                return $"Error:{ex.Message}";
            }

            return "OK";
        }

        /// <summary>
        /// 新增或覆寫資料至Table儲存區。
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public string InsertOrReplace(T entity)
        {
            try
            {
                TableResult retrievedResult = GetServerTableResult(entity);
                T updateEntity = (T)retrievedResult.Result;

                if (updateEntity != null)
                {
                    // Replace
                    updateEntity = entity;
                    TableOperation insertOrReplaceOperation = TableOperation.InsertOrReplace(updateEntity);
                    try
                    {
                        table.Execute(insertOrReplaceOperation);
                        return "OK";
                    }
                    catch (Exception ex)
                    {
                        return $"Error:{ex.Message}";
                    }
                }
                else
                {
                    // Insert
                    TableOperation insertOrReplaceOperation = TableOperation.InsertOrReplace(entity);
                    try
                    {
                        table.Execute(insertOrReplaceOperation);
                        return "OK";
                    }
                    catch (Exception ex)
                    {
                        return $"Error:{ex.Message}";
                    }
                }
            }
            catch (Exception ex)
            {
                return $"Error:{ex.Message}";
            }
        }

        /// <summary>
        /// 批次新增資料至Table儲存區。
        /// </summary>
        /// <param name="entities">欲快取的集合</param>
        /// <returns></returns>
        public string BatchInsert(IEnumerable<T> entities)
        {
            TableBatchOperation batchOperation = new TableBatchOperation();
            foreach (var cache in entities)
            {
                batchOperation.Insert(cache);
            }

            try
            {
                table.ExecuteBatch(batchOperation);
            }
            catch (Exception ex)
            {
                return $"Error:{ex.Message}";
            }

            return "OK";
        }

        /// <summary>
        /// 取得 table 所有集合。
        /// </summary>
        /// <returns></returns>
        public IEnumerable<T> GetAll()
        {
            TableQuery<T> query = new TableQuery<T>().Where(
                TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, tableName));
            return table.ExecuteQuery(query);
        }

        /// <summary>
        /// 以非同步取得大量(1000個以上)實體。
        /// </summary>
        /// <returns></returns>
        public async Task<IEnumerable<T>> GetAllAsync()
        {
            TableQuery<T> tableQuery = new TableQuery<T>();
            TableContinuationToken continuationToken = null;
            do
            {
                // 接收一個段落資料(1000以上實體)
                TableQuerySegment<T> tableQueryResult =
                    await table.ExecuteQuerySegmentedAsync(tableQuery, continuationToken);
                continuationToken = tableQueryResult.ContinuationToken;
                return tableQueryResult.Results;
            } while (continuationToken != null);
        }

        /// <summary>
        /// 以 rowkey 為關鍵字,取得相關集合。
        /// </summary>
        /// <param name="rowkey"></param>
        /// <returns></returns>
        public IEnumerable<T> GetRange(string rowkey)
        {
            TableQuery<T> rangeQuery = new TableQuery<T>().Where(
                TableQuery.CombineFilters(
                    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, tableName),
                    TableOperators.And,
                    TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThan, rowkey)));

            return table.ExecuteQuery(rangeQuery);
        }

        /// <summary>
        /// 取得單一實體
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public T GetSingle(T entity)
        {
            TableResult retrievedResult = GetServerTableResult(entity);
            T singleData = (T)retrievedResult.Result;
            if (singleData != null)
            {
                return singleData;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// 刪除實體
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public string Delete(T entity)
        {
            TableResult retrievedResult = GetServerTableResult(entity);
            T deleteEntity = (T)retrievedResult.Result;
            if (deleteEntity != null)
            {
                TableOperation deleteOperation = TableOperation.Delete(deleteEntity);
                table.Execute(deleteOperation);
                return "OK";
            }
            else
            {
                return "Error: deleteEntity is null.";
            }
        }

        /// <summary>
        /// 刪除資料表。
        /// 在刪除後一段時間內,將無法重新建立同名表格。
        /// </summary>
        /// <returns></returns>
        public string DeleteTable()
        {
            try
            {
                table.DeleteIfExists();
                return "OK";
            }
            catch (Exception ex)
            {
                return $"Error:{ex.Message}";
            }

        }

        /// <summary>
        /// 取回伺服器上的Table結果集
        /// </summary>
        /// <param name="tableData"></param>
        /// <returns></returns>
        private TableResult GetServerTableResult(T tableData)
        {
            TableOperation retrieveOperation = TableOperation.Retrieve<T>(tableName, tableData.RowKey.ToString());
            try
            {
                TableResult retrievedResult = table.Execute(retrieveOperation);
                return retrievedResult;
            }
            catch (Exception)
            {
                return null;
            }

        }
    }  
 

以上程式碼有誤,最新版請參考 GenericsAzureTableStorageAPI 專案。原程式碼不再修正。

完整專案我分享在 GitHub - https://github.com/kkbruce/GenericsAzureTableStorageAPI上,唯一要注意的是,如果要執行測試前請先啟動本機儲存體模擬器

GenericsAzureTableStorageAPI 測試結果

話說,一開始設計時where T沒加到new(),相關的 Get 方法會一直出現紅底線,查了(想了)好久才想到,為什麼 Azure Storage Table SDK 要對繼承自 TableEntity 做一個建構式限制,後來想通知加上 new() 之後就解決了。

3 則留言:

  1. 批量新增的部分 一次最高僅能100筆 否則會報錯 這是表的限制...

    回覆刪除
    回覆
    1. 感謝提醒,我找個時間 Update 一下 GitHub。

      刪除
    2. @Alan Liu 我已經更新 GitHub 的範例程式。;-)

      刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。