2015年3月21日

IQueryable の仕組みについて

Entity Framework (以下、EF)LINQ to Entities を実現するための IQueryable インターフェイスについて、今まであまり理解してなかったので MSDN とソースからお勉強。
理解してなくても使える EF は凄い。

IQueryable と IQueryProvider


public interface IQueryable : IEnumerable {
    Expression Expression { get; }
    Type ElementType { get; }
    IQueryProvider Provider { get; }
}
public interface IQueryable<out T> : IEnumerable<T>, IQueryable { }

public interface IQueryProvider{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);

    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

ソースの通りなのだが、とりあえず要点をまとめると
  • IQueryable.Expression は式木(式ツリー)を保持 (IQueryable の本体)
  • IQueryable.ProviderIQueryProvider を提供
  • IQueryable の通常版・ジェネリック版の違いは、継承元の IEnumerable がジェネリック版かどうか
  • IQueryProvider.CreateQuery(Expression) は引数の式木から IQueryable インスタンスを作成する
    (主に IQueryable の拡張メソッドで使用される)
  • IQueryProvider.Execute(Expression) は引数の式木を実行する
    (LINQ to Entities なら式木から SQL を構築して実行し、結果を返す)

IQueryable と IQueryProvider の実装方法は下記参照。

Queryable 拡張メソッド(IQueryable の標準クエリ演算子)

System.Linq.Queryable クラスの拡張メソッドの処理についてソースから説明。

[Queryable.Select の実装 (見易さのため一部整形)]

public static IQueryable<TResult> Select<TSource,TResult>(
    this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector
) {
    if (source == null) throw Error.ArgumentNull("source");
    if (selector == null) throw Error.ArgumentNull("selector");

    return source.Provider.CreateQuery<TResult>( 
        Expression.Call(
            null,
            ((MethodInfo)MethodBase.GetCurrentMethod())
                .MakeGenericMethod(typeof(TSource), typeof(TResult)), 
            new Expression[] { source.Expression, Expression.Quote(selector) }
        )
    );
}
[行毎の説明]
4-5 引数の null チェック
7 IQueryable インスタンスを新たに作成して return
※ IQueryable はイミュータブルなので、式木を変更するには新規作成が必要
8 IQueryProvider.CreateQuery の引数 : Expression.Call はメソッド呼び出しの式木を作成
9 Expression.Call の第 1 引数 : static メソッドを呼び出すので null を指定
10-11 Expression.Call の第 2 引数 : 自身 (Queryable.Select) を呼び出しメソッドとして指定
12 Expression.Call の第 3 引数 : 呼び出すメソッドの引数を式木で指定
1 個目の引数は IQueryable.Expression を使用し、2 個目の引数は selector 引数を使用
Expression.Quote は式木型引数を表すために使うものらしい

式木に触ったことが無い場合は意味不明かもしれないが、やってることは単純で、自身のメソッド呼び出しを式木に変換しているだけである。
Queryable クラスの他の拡張メソッドも、一部を除いてほぼ同じ実装。
(こういう実装の場合、CSS のクラスみたくメソッドの定義に対してカンマ区切りが使えれば理想かも)

また、式木 (Expression 型) は、ToString() で簡単に中身を見ることができる。

[Where と Select を使った式木の表示]

var source = new[]{ 0, 1, 2, 3, 4, 5 };
// 純粋な IEnumerable に対する AsQueryable() は EnumerableQuery を生成
var query =
    from n in source.AsQueryable()
    where n % 2 == 1
    select n * n;

Console.WriteLine(query.Expression);
Console.WriteLine(string.Join(",", query));
[結果]
System.Int32[].Where(n => ((n % 2) == 1)).Select(n => (n * n))
1,9,25

まとめ

  • IQueryable は式木を構築するためのインターフェース
  • Queryable 拡張メソッドは式木を簡単に構築するためのユーティリティ

Expression.Quote の存在に疑問を持った場合は下記が参考になる。

0 件のコメント:

コメントを投稿