Azure Storage の SDK (Windows Azure Storage) を利用して .NET のクライアントから Table のデータを取得する際に、
検索条件を指定しようとすると、通常の実装では次のようなコードになり少し複雑です。
この例では、フィルター条件を 2 つ指定しています。
var query = new TableQuery<Person>()
.Where(TableQuery.CombineFilters(
TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "2015"),
TableOperators.And,
TableQuery.GenerateFilterConditionForInt("Age", QueryComparisons.LessThan, 20)));var result = PeopleTable.ExecuteQuery(query).ToArray();
ここで、Person は TableEntity を継承したクラスで、PeopleTable は CloudTable 型のオブジェクトです。
結局、上記の Where メソッドに渡されるのは、
(PartitionKey eq ‘2015’) and (Age lt 20)
という文字列になります。
これなら string.Format メソッドでもよいのではないのかという気もしますが、
プログラミングのミスを防ぐためには、フィルターや射影などの検索条件は LINQ で指定したいところです。
検索条件を LINQ で指定する方法として TableServiceContext クラスを使う方法もあるようですが、
現在は Obsolete 属性が指定されており、非推奨となっています。
とはいえ、自力で IQueryable<T> を実装するのも骨が折れるので、
ここでは簡易的に、TableQuery<T> の拡張メソッドとして Select および Where メソッドを実装していきます。
ラムダ式で指定された検索条件を式ツリーとして受け取って解析し、動的にクエリを生成します。
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using Microsoft.WindowsAzure.Storage.Table; | |
namespace ExpressionsConsole | |
{ | |
public static class TableHelper | |
{ | |
static readonly MethodInfo String_CompareTo = typeof(string).GetMethod("CompareTo", new[] { typeof(string) }); | |
public static TableQuery<TElement> Select<TElement>(this TableQuery<TElement> query, Expression<Func<TElement, object>> selector) | |
{ | |
if (query == null) throw new ArgumentNullException("query"); | |
if (selector == null) throw new ArgumentNullException("selector"); | |
var @new = selector.Body as NewExpression; | |
if (@new == null) throw new InvalidOperationException(); | |
query.SelectColumns = @new.Constructor.GetParameters().Select(p => p.Name).ToArray(); | |
return query; | |
} | |
public static TableQuery<TElement> Where<TElement>(this TableQuery<TElement> query, Expression<Func<TElement, bool>> predicate) | |
{ | |
if (query == null) throw new ArgumentNullException("query"); | |
if (predicate == null) throw new ArgumentNullException("predicate"); | |
var binary = predicate.Body as BinaryExpression; | |
if (binary == null) throw new InvalidOperationException(); | |
var filter = GenerateFilter(binary); | |
query.FilterString = string.IsNullOrWhiteSpace(query.FilterString) ? filter : TableQuery.CombineFilters(query.FilterString, TableOperators.And, filter); | |
return query; | |
} | |
static string GenerateFilter(BinaryExpression binary) | |
{ | |
switch (binary.NodeType) | |
{ | |
case ExpressionType.AndAlso: | |
case ExpressionType.OrElse: | |
return CombineFilters(binary); | |
case ExpressionType.Equal: | |
case ExpressionType.NotEqual: | |
case ExpressionType.GreaterThan: | |
case ExpressionType.GreaterThanOrEqual: | |
case ExpressionType.LessThan: | |
case ExpressionType.LessThanOrEqual: | |
return (binary.Left is MethodCallExpression) ? GenerateFilterConditionForMethodCall(binary) : GenerateFilterCondition(binary); | |
default: | |
throw new InvalidOperationException(); | |
} | |
} | |
static string CombineFilters(BinaryExpression binary) | |
{ | |
var left = binary.Left as BinaryExpression; | |
if (left == null) throw new InvalidOperationException(); | |
var right = binary.Right as BinaryExpression; | |
if (right == null) throw new InvalidOperationException(); | |
var op = ToCombinationOperator(binary.NodeType); | |
return TableQuery.CombineFilters(GenerateFilter(left), op, GenerateFilter(right)); | |
} | |
static string GenerateFilterCondition(BinaryExpression binary) | |
{ | |
var left = binary.Left as MemberExpression; | |
if (left == null) throw new InvalidOperationException(); | |
var op = ToComparisonOperator(binary.NodeType); | |
var rightValue = binary.Right.Invoke(); | |
return | |
left.Type == typeof(byte[]) ? TableQuery.GenerateFilterConditionForBinary(left.Member.Name, op, (byte[])rightValue) : | |
left.Type == typeof(bool) ? TableQuery.GenerateFilterConditionForBool(left.Member.Name, op, (bool)rightValue) : | |
left.Type == typeof(DateTime) ? TableQuery.GenerateFilterConditionForDate(left.Member.Name, op, (DateTime)rightValue) : | |
left.Type == typeof(DateTimeOffset) ? TableQuery.GenerateFilterConditionForDate(left.Member.Name, op, (DateTimeOffset)rightValue) : | |
left.Type == typeof(double) ? TableQuery.GenerateFilterConditionForDouble(left.Member.Name, op, (double)rightValue) : | |
left.Type == typeof(Guid) ? TableQuery.GenerateFilterConditionForGuid(left.Member.Name, op, (Guid)rightValue) : | |
left.Type == typeof(int) ? TableQuery.GenerateFilterConditionForInt(left.Member.Name, op, (int)rightValue) : | |
left.Type == typeof(long) ? TableQuery.GenerateFilterConditionForLong(left.Member.Name, op, (long)rightValue) : | |
TableQuery.GenerateFilterCondition(left.Member.Name, op, rightValue.To<string>()); | |
} | |
static string GenerateFilterConditionForMethodCall(BinaryExpression binary) | |
{ | |
var methodCall = binary.Left as MethodCallExpression; | |
if (methodCall == null) throw new InvalidOperationException(); | |
if (methodCall.Method != String_CompareTo) throw new InvalidOperationException(); | |
var left = methodCall.Object as MemberExpression; | |
if (left == null) throw new InvalidOperationException(); | |
var op = ToComparisonOperator(binary.NodeType); | |
var rightValue = methodCall.Arguments[0].Invoke(); | |
return TableQuery.GenerateFilterCondition(left.Member.Name, op, rightValue.To<string>()); | |
} | |
static string ToCombinationOperator(ExpressionType nodeType) | |
{ | |
switch (nodeType) | |
{ | |
case ExpressionType.AndAlso: | |
return TableOperators.And; | |
case ExpressionType.OrElse: | |
return TableOperators.Or; | |
default: | |
throw new InvalidOperationException(); | |
} | |
} | |
static string ToComparisonOperator(ExpressionType nodeType) | |
{ | |
switch (nodeType) | |
{ | |
case ExpressionType.Equal: | |
case ExpressionType.NotEqual: | |
case ExpressionType.GreaterThan: | |
case ExpressionType.GreaterThanOrEqual: | |
case ExpressionType.LessThan: | |
case ExpressionType.LessThanOrEqual: | |
return (string)typeof(QueryComparisons).GetField(nodeType.ToString()).GetValue(null); | |
default: | |
throw new InvalidOperationException(); | |
} | |
} | |
} | |
} |
using System; | |
using System.Collections.Generic; | |
using System.Configuration; | |
using System.Diagnostics; | |
using System.Linq; | |
using Microsoft.WindowsAzure.Storage; | |
using Microsoft.WindowsAzure.Storage.Table; | |
namespace ExpressionsConsole | |
{ | |
public static class TableTest | |
{ | |
static readonly CloudTable PeopleTable; | |
static TableTest() | |
{ | |
var accountString = ConfigurationManager.ConnectionStrings["StorageAccount"].ConnectionString; | |
var account = CloudStorageAccount.Parse(accountString); | |
var tableClient = account.CreateCloudTableClient(); | |
PeopleTable = tableClient.GetTableReference("people"); | |
} | |
public static void SelectTest() | |
{ | |
var query = new TableQuery<Person>() | |
.Select(p => new { p.FirstName, p.Age }); | |
var result = PeopleTable.ExecuteQuery(query).ToArray(); | |
} | |
public static void WhereTest1() | |
{ | |
// (PartitionKey eq '2015') and ((LastName ge 'W') or (Age lt 20)) | |
var query = new TableQuery<Person>() | |
.Where(p => p.PartitionKey == "2015" && (p.LastName.CompareTo("W") >= 0 || p.Age < 20)); | |
var result = PeopleTable.ExecuteQuery(query).ToArray(); | |
} | |
public static void WhereTest2() | |
{ | |
// (PartitionKey eq '2015') and ((LastName ge 'W') or (Age lt 20)) | |
var query = new TableQuery<Person>() | |
.Where(p => p.PartitionKey == "2015") | |
.Where(p => p.LastName.CompareTo("W") >= 0 || p.Age < 20); | |
var result = PeopleTable.ExecuteQuery(query).ToArray(); | |
} | |
public static void WhereSelectTest() | |
{ | |
var query = new TableQuery<Person>() | |
.Where(p => p.PartitionKey == "2015") | |
.Where(p => p.LastName.CompareTo("W") >= 0 || p.Age < 20) | |
.Select(p => new { p.FirstName, p.Age }); | |
var result = PeopleTable.ExecuteQuery(query).ToArray(); | |
} | |
} | |
[DebuggerDisplay(@"\{{PartitionKey}/{RowKey}\}")] | |
public class Person : TableEntity | |
{ | |
public string FirstName { get; set; } | |
public string LastName { get; set; } | |
public int Age { get; set; } | |
} | |
} |
このような TableHelper クラスを実装することで、Azure Table の検索条件を LINQ で指定できるようになります。
ただし、文字列の不等式については、String クラスの演算子として不等号が定義されていないため、
p.LastName >= "W"
と書くことができず、
p.LastName.CompareTo("W") >= 0
のようにせざるを得ませんでした。
作成したサンプル
ExpressionsConsole (GitHub)
バージョン情報
Windows Azure Storage 6.0.0
参照
Windows Azure Storage
Expression<TDelegate> クラス
TableQuery<TElement> Class
Windows Azure Storage Extensions
テーブル サービスに対する LINQ クエリの作成 (古い形式)
TableServiceContext Class (古い形式)