Azure Table の検索条件を LINQ で指定する

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 メソッドを実装していきます。
ラムダ式で指定された検索条件を式ツリーとして受け取って解析し、動的にクエリを生成します。

このような 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 (古い形式)

広告

エンティティを匿名型で手軽に実装する (2)

前回のエンティティを匿名型で手軽に実装する (1) で作成した EntityType<TEntity> では、
インスタンスを初期化するために毎回 ConstructorInfo を経由していましたが、
少し重い処理のため、回数が多いと処理時間に影響します。

そこで、あらかじめコンストラクターをラムダ式の式ツリーからコンパイルしておくという方法があります。
先にコードを示します。

取得した ConstructorInfo をもとに、

p => new TEntity((int)p[0], (string)p[1])

のような処理に相当する式ツリーを構築してコンパイルすることで、object[] から TEntity を初期化する関数が得られます。

CreateEntity メソッドの呼び出しを 100 万回実行して計測してみると、手元の環境だと前回のコードでは

  • EntityType の初期化: 0.0006 秒
  • CreateEntity メソッド 100 万回: 0.6 秒

だったのが、今回のコードでは

  • EntityType の初期化: 0.006 秒
  • CreateEntity メソッド 100 万回: 0.06 秒

になりました。

 

前回: エンティティを匿名型で手軽に実装する (1)

作成したサンプル
ExpressionsConsole (GitHub)

参照
Expression<TDelegate> クラス

メソッドの引数の名前を式ツリーから取得する

メソッドに渡された引数の値が null でないことや空文字でないことなどを検証するために、
次のようなコードを記述することと思います。


static void Method1(string parameter1)
{
    if (parameter1 == null) throw new ArgumentNullException("parameter1");
    if (parameter1.Length == 0) throw new ArgumentException("値を空にすることはできません。", "parameter1");

    // 以下省略。
}


このようなコードは頻繁に必要になるので、コード スニペットを用意しておくと便利です

メソッド チェーンにしたい場合は、次のように拡張メソッドを使います。


static class Program
{
    static string[] Separate(this string text)
    {
        return text
            .AssertArgumentNotNull("text")
            .AssertArgumentNotEmpty("text")
            .Split(‘:’)
            .Select(s => s.Trim())
            .ToArray();
    }
}

[DebuggerNonUserCode]
public static class Assert
{
    public static T AssertArgumentNotNull<T>(this T value, string name) where T : class
    {
        if (value == null) throw new ArgumentNullException(name); 
        return value;
    }

    public static string AssertArgumentNotEmpty(this string value, string name)
    {
        if (value.Length == 0) throw new ArgumentException("値を空にすることはできません。", name); 
        return value;
    }
}


上記の方法ではメソッドの引数の名前を文字列のリテラル ("parameter1" のような形式) で記述することになりますが、
どうしてもこれを避けたい場合、ラムダ式による式ツリー (式木, Expression Tree) を利用することによって実現できます。


static class Program
{
    static void Method1(string parameter1)
    {
        Assert.IsArgumentNotNull(() => parameter1);
        Assert.IsArgumentNotEmpty(() => parameter1);

        // 以下省略。
    }
}

[DebuggerNonUserCode]
public static class Assert
{
    public static void IsArgumentNotNull<T>(Expression<Func<T>> getValue) where T : class
    {
        var body = (MemberExpression)getValue.Body;
        var field = (FieldInfo)body.Member;
        var value = getValue.Compile()();

        if (value == null) throw new ArgumentNullException(field.Name);
    }

    public static void IsArgumentNotEmpty(Expression<Func<string>> getValue)
    {
        var body = (MemberExpression)getValue.Body;
        var field = (FieldInfo)body.Member;
        var value = getValue.Compile()();

        if (value.Length == 0) throw new ArgumentException("値を空にすることはできません。", field.Name);
    }
}


Expression<TDelegate>.Compile メソッドを呼び出すことで、式ツリーをデリゲートに変換しています。

バージョン情報
.NET Framework 3.5, 4

参照
式ツリー (C# および Visual Basic)

カテゴリー: .NET Framework. タグ: . Leave a Comment »