kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

タグアーカイブ: Queryable

他のQueryProviderに寄生するQueryProviderの作り方


Expression の解釈に介入したい場合とかに利用するパターンなんですが、別のQueryProviderに寄生するQueryProvider を書く事がよくありますんで、コード例おば。
 
public partial class InterceptorQueryProvider : IQueryProvider
{
    IQueryProvider originalProvider;

    public InterceptorQueryProvider( IQueryProvider originalProvider ) { this.originalProvider = originalProvider; }

    public IQueryable CreateQuery( Expression exp )
    {
        return new WeapperQueryableNoType( this,originalProvider.CreateQuery( exp ) );
    }

    public IQueryable<T> CreateQuery<T>( Expression exp )
    {
        return new WrapperQueryable<T>( this,originalProvider.CreateQuery<T>( exp ) );
    }

 
    public object Execute( Expression exp )
    {
        //ここで Expression に対して介入することができる
        return originalProvider.Execute( exp );
    }
    public IEnumerable<T> Execute<T>( Expression exp )
    {
        //ここでExpressionに対して介入することができる
        return originalProvider.Execute<T>( exp );
    }
}
 
public class WrapperQueryable<T> : IQueryable<T>
{
    readonly IQueryable<T> Query;
    readonly InterceptorQueryProvider provider;
    public WrapperQueryable(InterceptorQueryProvider p, IQueryable<T> q )
    {
        this.provider = p;
        this.Query = q;
    }
    public IEnumerable<T> GetEnumerator()
    {
        return this.Query.GetEnumerator<T>();
    }
    public IEnumerable GetEnumerator()
    {
         return this.Query.GetEnumerator();
    }
    public Type ElementType { get { return this.Query.ElementType; } }
    public Expression Expression { get { return this.Query.Expression; } }
    public IQueryProvider Provider { get { return this.provider; } } // Provider だけ置き換える
}
public class WrapperQueryableNoType : IQueryable
{
    readonly IQueryable Query;
    readonly InterceptorQueryProvider provider;
    public WrapperQueryable(InterceptorQueryProvider p, IQueryable q )
    {
        this.provider = p;
        this.Query = q;
    }
    public IEnumerable GetEnumerator()
    {
        return this.Query.GetEnumerator();
    }
    public Type ElementType { get { return this.Query.ElementType; } }
    public Expression Expression { get { return this.Query.Expression; } }
    public IQueryProvider Provider { get { return this.provider; } } // Provider だけ置き換える
}
 
ADO.NET Data Service Client の場合には CreateQuery の戻りが IQueryable なので以下の要領で Intercepter をはさみます
var interceptor = new InterceptorQueryProvider();
return WrapperQueryable<T>( interceptor, dataServiceContext.CreateQuery( entitySetName ) );
 
介入できることのメリットとしては特定の QueryProviderでサポートされない要素をExpressionからそぎ落として抽出をかけるようにするとか、 そぎ落とした操作を Queryable -> Enumerable へ変換する事で、クエリ実行の一部をローカル化する等があります。たとえばAzureではサポートされない集計演算や、order by 操作をクエリ対象で実行するのでなくローカル実行する様に式木を変形してしまえばアプリケーションコードのあちこちを直すのでは無くてミドルとして割り込む形でプラットフォームに無い機能の実装を提供できるでしょう。
 
ちなみに式木をローカル実行する場合には Expression.Lambda で包み、CompileしてDelegateにしてからDynamicInvoke で実行します。リモート実行したい部分についてリモートするメソッドを以下のように用意したとすると
 
public abstract IEnumerable<T> ExecuteRemote( Expression t )
{
}
 
式木の一部をリモート実行する部分については
Expression.Call( Expression.Constant(this), this.GetType().GetMethod(“ExecuteRemote”), Expression.Constant( リモートしたい Expression ) )
 
といった変換が必要になるでしょう。 Queryable.Concat の source1, source2 をリモートでそれぞれ実行して結果をローカルで Concat するとすれば ExpressionVisitor 派生で以下のようにコードします
 
// こういう初期化がされているとして
MethodInfo remoteMethod = typeof(Interceptor).GetMethod(“ExecuteRemote”)
MethodInfo[] enumerableMethods = typeof(Enumerable).GetMethods();
// こう
public override Expression VisitMethodCall( MethodCallExpression node )
{
    if( node.Method.DeclaringType == typeof( Queryable ) && node.Method.Name==”Concat” ) )
    {
         return Expression.Call( enumerableMethods.Where( m=>m.Name==”Concat” ).Single().MakeGenericMethod( elementType ),
                      Expression.Call( Expression.Constant(this), remoteMethod, Expression.Constant( node.Arguments[0] ) ),
                      Expression.Call( Expression.Constant(this), remoteMethod, Expression.Constant( node.Arguments[1] ) ) );
    }
    …
}
 
要するに以下のコードに落ちるわけですね。
 
 Enumerable.Concat<elementType>( this.ExecuteRemote( source1), this.ExecuteRemote(source2) );
 
Queryableには同等のEnumerableメソッドが必ずありますのでローカル実行に入った後の部分は機械的に変換する事ができますが、Generic パラメータが絡んでいるとリフレクションでのGetMethod の結果は激しくあてにならないので( Enumerable.Concat を探そうとして GetMethod( “Concat”, new Type[]{ typeof(IEnumerable<>),typeof( IEnumerable<>) } ) で検索すると出てこないあたり)、ぶっちゃけてマッピング用の辞書は static コンストラクタで構築すること推奨します。(全部列挙しちゃって合うはずのものを Where で抽出して MakeGenericMethod で特化させて使うのが安心かも)。
 
クエリを示す式木のどこがリモート実行可能かとかは下請けに使う QueryProvider 次第だったりしますが Queryable -> Enumerable の変換をしてローカル化してしまえば KVS でjoin できないとかも、ローカルでIEnumerableになってからjoin されるのでリクエストが複数回飛ぶとか無駄要素も拾う可能性とかもろもろの性能面の我慢のしようによっては実行可能という落としどころが得られます。最悪ケースとしてExpressionから必要なIndexを探してみてあればそれを単純に読む、無ければ WorkerRoleにExpressionをうまいこと文字列化して送って「インデックスつくってー」って悲鳴を上げて(Queue書きして) HttpException で 503 Service Unavaiable 投げちゃうのも決して適切とはいえないかも知れませんがとりうる実装のひとつとなります。
(アプリケーション内で使うすべての Expression を取得する方法は .NET Framework内には無かったりしますので、そういうことをする方法をアプリケーションレベルで用意してないと必要なインデックスが準備できてるかは判断できませんのよ!!)
 
Concat とかは単純だったりしますけど、Where句のand項を別々のクエリに振り分ける(例:Queryable.Where( Queryable.SelectMeny( X, Y) , q => q.X.a && q.Y.b )  を Queryable.SelectMeny( Queryable.Where( X , e => e.a ), Queryable.Where( Y, e=>e.b ) ) から Enumerable.SelectMeny( remote1でWhere、remote2でWhere) にしないと各Whereがちゃんと実行できないとか) とかはまじめに実装すると結構大変です。