Azure 基礎:自定義 Table storage 查詢條件

sparkdev發表於2017-03-17

本文是在 《Azure 基礎:Table storage》 一文的基礎上介紹如何自定義 Azure Table storage 的查詢過濾條件。如果您還不太清楚 Azure Table storage 的基本用法,請先移步前文

讓我們回到前文中提到的一個問題,如何過濾出 MyLogTable 表中某一天產生的所有日誌?在進入細節前我們先來回顧一下 MyLogTable 類的設計:

internal class MyLogEntity : TableEntity
{
    public MyLogEntity() { }
    public MyLogEntity(string pkey, string rkey)
    {
        this.PartitionKey = pkey;
        this.RowKey = rkey;
    }
    //
} 

PartitionKey 用來存放產生日誌的年份和月份(例如201607表示2016年7月),RowKey 用來存放產生日誌的天和時分秒毫秒(例如160934248492表示16號9點34分…)。在我們設計的 MyLogTable 中,天資訊儲存在 RowKey 的前兩位。我們要做的就是過濾 RowKey 的前兩位,也就是找到所有 RowKey 以"xx"開頭的記錄。這在字串操作中稱為 StartsWith。遺憾的是現有 Table storage 的介面中沒有提供這種功能的方法,因此我們需要自己實現它(還好 TableQuery 的實現支援我們去擴充套件它)!本文將透過實現 StartsWith 過濾條件說明如何自定義 Azure Table storage 的查詢過濾條件。

TableQuery 類

TableQuery 是本文的主角,它代表了某個表上的一個查詢。基本用法是使用查詢條件構建一個 TableQuery 類的例項,然後把這個例項作為引數傳遞給 CloudTable 的ExecuteQuery 方法:

TableQuery<MyLogEntity> query = new TableQuery<MyLogEntity>().Where(
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "201607")
);
var queryResult = logTable.ExecuteQuery(query);

我們還可以使用 TableQuery 的靜態方法 CombineFilters 構建自定義的查詢條件。比如我們要查詢 PartitionKey 等於 "201607" 並且 RowKey 等於"161148372454"的記錄:

TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "201607"),
    TableOperators.And,
    TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, "161148372454")
);

此時函式的返回結果為: "(PartitionKey eq '201607') and (RowKey eq '161148372454')"。
然後把這個過濾字串送給 query.Where 函式做引數,或者設定給 query.FilterString 屬性,就可以完成過濾功能了。
CombineFilters 方法可愛的地方在於我們可以不斷的用它來合併查詢條件,直到滿意為止!

接下來我們一起看看 StartsWith 過濾條件的實現過程。

比較字串

如何從一些字串中找出以某個子串開頭的字串呢?我們可以從字串的比較入手。
比如字串具有下面的關係:

“abc”  ==  “abc” < “abd”
“abc” < “abca” < “abd”
“abc” < “abcz” < “abd”

由上面的大小關係我們可以得出結論:以"abc"開頭的字串必定大於或等於"abc"且小於"abd"。OK,這就是我們構建 StartsWith 過濾條件的理論基礎。

構建 StartsWith 過濾條件

接下來我們透過 TableQuery.CombineFilters 方法構建 StartsWith 過濾條件:

string startsWithCondition = TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, "abc"),
    TableOperators.And,
    TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThan, "abd")
);

TableQuery.CombineFilters 方法的返回值是一個字串。執行上面的程式碼我們會得到字串:

"(RowKey ge 'abc') and (RowKey lt 'abd')"

我們完全可以手動拼出這樣的字串,但我相信沒有程式猿願意這麼幹。所以我們要繼續完善上面的方法:

string startStr = "abc";
int endIndex = startStr.Length - 1;
Char lastChar = startStr[endIndex];
// 找到比字元'c'大的那個字元。
Char afterLastChar = (char)(lastChar + 1);
// 拼出字串 "abd"
string endStr = startStr.Substring(0, endIndex) + afterLastChar;
string startsWithCondition = TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, startStr),
    TableOperators.And,
    TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThan, endStr)
);

組合更多過濾條件

在前面構建 StartsWith 過濾條件時我們已經使用 TableQuery.CombineFilters 方法組合了不同的過濾條件。遺憾的是 TableQuery.CombineFilters 方法只有兩個引數的過載,我們不能新增更多的 TableOperators 操作。

但我們可以繼續呼叫 TableQuery.CombineFilters 方法去組合上一個結果和新的條件。比如我們要把 Startswith 過濾條件和 PartitionKey 過濾條件組合起來就可以這麼幹:

string filterCondition = TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "201607"),
    TableOperators.And,
    "(RowKey ge 'abc') and (RowKey lt 'abd')"
);

執行上面的程式碼,生成的結果為:

(PartitionKey eq '201607') and ((RowKey ge 'abc') and (RowKey lt 'abd'))

到這來就很清楚了,TableQuery.CombineFilters 方法的主要工作就是把過濾條件組織成查詢引擎能夠識別的字串。因而我們可以透過不斷的疊加生成很複雜的過濾條件。

封裝 StartsWith 過濾條件

下面我們把 StartsWith 的邏輯封裝到 StartsWithByRowKey 型別中,下面是完整的程式碼:

public class MyLogEntity : TableEntity
{
    public MyLogEntity() { }
    public MyLogEntity(string pkey, string rkey)
    {
        this.PartitionKey = pkey;
        this.RowKey = rkey;
    }

    public DateTime LogDate { get; set; }
    public string LogMessage { get; set; }
    public string ErrorType { get; set; }
}

public class StartsWithByRowKey : IQuery<CloudTable, List<MyLogEntity>> { private readonly string partitionKey; private readonly string startsWithString;
internal StartsWithByRowKey(string partitionKey, string startsWithString) { this.partitionKey = partitionKey; this.startsWithString = startsWithString; } public List<MyLogEntity> Execute(CloudTable coludTable) { var query = new TableQuery<MyLogEntity>(); int endIndex = startsWithString.Length - 1; Char lastChar = startsWithString[endIndex]; Char afterLastChar = (char)(lastChar + 1); string endStr = startsWithString.Substring(0, endIndex) + afterLastChar; string startsWithCondition = TableQuery.CombineFilters( TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, startsWithString), TableOperators.And, TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThan, endStr) ); string filterCondition = TableQuery.CombineFilters( TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey), TableOperators.And, startsWithCondition ); var entities = coludTable.ExecuteQuery(query.Where(filterCondition)); return entities.ToList(); } } public interface IQuery<in TModel, out TResult> { TResult Execute(TModel model); }

應用 StartsWith 的例項

現在查詢 PartitionKey 為"201607",RowKey 以"16"開頭的記錄可以這麼寫:

StartsWithByRowKey myStartsWithQuery = new StartsWithByRowKey("201607", "16");
List<MyLogEntity> result = myStartsWithQuery.Execute(logTable);

程式碼簡潔了很多,讀起來也更清晰了(您還可以動手給 PartitionKey 也新增同樣的功能)!

總結

本文簡單的介紹了 TableQuery 型別,然後比較詳細的說明了 StartsWith 過濾條件的思路及實現。主要是想透過 StartsWith 的實現來說明如何利用現有的型別及方法來實現自定義查詢的過濾條件。對於有類似需求的朋友,希望能起到拋磚引玉的作用。

相關文章