IQueryableおよびLINQデータプロバイダーの仕組み

.NET開発者は、LINQツールを使用して、メモリ内のオブジェクトのコレクションと、データベースまたは他のリモートソースに格納されているオブジェクトの両方を一貫した方法で操作できます。 たとえば、メモリ内のリストとEntity Frameworkを使用するデータベースから10個の赤いリンゴを要求するには、まったく同じコードを使用できます。



List<Apple> appleList; DbSet<Apple> appleDbSet; var applesFromList = appleList.Where(apple => apple.Color == “red”).Take(10); var applesFromDb = appleDbSet.Where(apple => apple.Color == “red”).Take(10);
      
      





ただし、これらの要求はさまざまな方法で実行されます。 最初の場合、foreachを使用して結果を列挙するとき、リンゴは指定された述語を使用してフィルターされ、その後、最初の10個が取得されます。 2番目の場合、クエリ式を含む構文ツリーは特別なLINQプロバイダーに渡され、データベースへのSQLクエリに変換されて実行されます。その後、見つかった10個のレコードのC#オブジェクトを形成し、それらを返します。 この動作は、外部データソースへのLINQプロバイダーを作成するように設計されたIQueryable <T>インターフェイスによって保証できます。 以下では、このインターフェイスの構成と使用の原則を理解しようとします。



インターフェイスIEnumerable <T>およびIQueryable <T>



一見、LINQはWhere()、Select()、First()、Count()などの一連の拡張メソッドに基づいているように見えるかもしれません。 IEnumerable <T>インターフェイスへ。これにより、開発者は、メモリ内のオブジェクト(LINQ to Objects)とデータベース(たとえばLINQ to SQL、LINQ to Entities)とリモートサービス(たとえばLINQ)の両方にクエリを均一に書き込むことができます。 OData Servicesへ)。 しかし、これはそうではありません。 実際には、IEnumerable <T>の拡張メソッド内で、シーケンスを使用した対応する操作が既に実装されています。 したがって、たとえば、最初の<TSource>メソッド(Func <TSource、bool>述語)は.Net Framework 4.5.2で実装されています。そのソースは次のようにここで利用できます。



 public static TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { if (source == null) throw Error.ArgumentNull("source"); if (predicate == null) throw Error.ArgumentNull("predicate"); foreach (TSource element in source) { if (predicate(element)) return element; } throw Error.NoMatch(); }
      
      





一般的な場合、このような方法はデータベースまたはサービスにあるデータに対して実行できないことは明らかです。 それを実行するには、データセット全体を直接アプリケーションに直接ロードすることしかできませんが、これは明らかな理由で受け入れられません。



アプリケーション外部のデータにLINQプロバイダーを実装するには、IQueryable <T>インターフェイス(IEnumerable <T>から継承)を、IEnumerable <T>向けに記述されたものとほぼ同じ拡張メソッドのセットと共に使用します。 List <T>がIEnumerable <T>を実装し、Entity FrameworkのDbSet <T>-IQueryable <T>を実装しているため、記事の冒頭にリストされているAppleクエリの実行方法は異なります。



IQueryable <T>の拡張メソッドの特徴は、データ処理ロジックが含まれていないことです。 代わりに、単純にリクエストの説明を含む構文構造を形成し、チェーン内のすべての新しいメソッド呼び出しでリクエストを「エスカレート」します。 集計メソッド(Count()など)を呼び出すとき、またはforeachを使用して列挙するとき、要求の説明はIQueryable <T>の特定の実装内にカプセル化されたプロバイダーに送信され、クエリは既に動作するデータソース言語に変換されています、実行します。 Entity Frameworkの場合、この言語はSQLです。MongoDbの.Netドライバーの場合、それは検索jsonオブジェクトなどです。



ところで、LINQプロバイダーのいくつかの「興味深い」特性は、この機能から得られます。



日曜大工LINQ:ISimpleQueryable <T>



IQueryable <T>インターフェイスデバイスについて説明する前に、そのシンプルなアナログであるISimpleQueryable <T>インターフェイスと、それに対するいくつかの拡張メソッドをLINQスタイルで記述してみましょう。 これにより、IQueryable <T>を使用する基本的な原則を、その実装のニュアンスに触れることなく明確に示すことができます。

 public interface ISimpleQueryable<TSource> : IEnumerable<TSource> { string QueryDescription { get; } ISimpleQueryable<TSource> CreateNewQueryable(string queryDescription); TResult Execute<TResult>(); }
      
      





インターフェイスには、クエリの説明を含むQueryDescriptionプロパティと、必要に応じてこのクエリを実行するExecute <TResult>()メソッドが表示されます。 実行の結果は列挙またはCount()などの集計関数の値になる可能性があるため、これは汎用メソッドです。 さらに、インターフェイスにはCreateNewQueryable()メソッドがあり、新しいLINQメソッドを追加するときに、新しいクエリの説明を使用して、新しいISimpleQueryable <T>を追加できます。 ここでのクエリの説明は文字列として表示され、LINQではExpression Treeが使用されます 。これについては、 hereまたはhereを参照してください



それでは、拡張メソッドに移りましょう。



 public static class SimpleQueryableExtentions { public static ISimpleQueryable<TSource> Where<TSource>(this ISimpleQueryable<TSource> queryable, Expression<Func<TSource, bool>> predicate) { string newQueryDescription = queryable.QueryDescription + ".Where(" + predicate.ToString() + ")"; return queryable.CreateNewQueryable(newQueryDescription); } public static int Count<TSource>(this ISimpleQueryable<TSource> queryable) { string newQueryDescription = queryable.QueryDescription + ".Count()"; ISimpleQueryable<TSource> newQueryable = queryable.CreateNewQueryable(newQueryDescription); return newQueryable.Execute<int>(); } }
      
      





ご覧のとおり、これらのメソッドはクエリの説明に自身に関する情報を追加し、ISimpleQueryable <T>の新しいインスタンスを作成するだけです。 さらに、IEnumerable <T>の対応するものとは異なり、Where()メソッドはFunc <TSource、bool>述部自体を受け入れませんが、前述の式ツリーとその式Expression <Func <TSource、bool>を受け入れます>。 この例では、述語コードで文字列を取得する機会を与えており、実際のLINQの場合-クエリのすべての詳細を式ツリーの形式で保存する機能を提供しています。



最後に、ISimpleQueryable <T>の簡単な実装を作成します。これには、実行方法を除き、LINQクエリの作成に必要なすべてが含まれます。 より現実的にするには、データソース(_dataSource)へのリンクを追加します。これは、Execute()メソッドを使用してクエリを実行するときに使用する必要があります。



 public class FakeSimpleQueryable<TSource> : ISimpleQueryable<TSource> { private readonly object _dataSource; public string QueryDescription { get; private set; } public FakeSimpleQueryable(string queryDescription, object dataSource) { _dataSource = dataSource; QueryDescription = queryDescription; } public ISimpleQueryable<TSource> CreateNewQueryable(string queryDescription) { return new FakeSimpleQueryable<TSource>(queryDescription, _dataSource); } public TResult Execute<TResult>() { //    QueryDescription     dataSource throw new NotImplementedException(); } public IEnumerator<TSource> GetEnumerator() { return Execute<IEnumerator<TSource>>(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
      
      





次に、FakeSimpleQueryableの簡単なクエリを検討します。



 var provider = new FakeSimpleQueryable<string>("", null); int result = provider.Where(s => s.Contains("substring")).Where(s => s != "some string").Count();
      
      





上記のコードが実行されたときに何が起こるかを理解してみましょう(下の図も参照)。







実際のIQueryable <T> ...およびIQueryProvider <T>



次に、.Netに実装されているIQueryable <T>インターフェースが何であるかを考えます。

 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); }
      
      





以下に注意してください。



ここで、Where()メソッドの例を使用して、IQueryable <T>の拡張メソッドの動作を見てみましょう。

 public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, int, bool>> predicate) { if (source == null) throw Error.ArgumentNull("source"); if (predicate == null) throw Error.ArgumentNull("predicate"); return source.Provider.CreateQuery<TSource>( Expression.Call( null, ((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(typeof(TSource)), new Expression[] { source.Expression, Expression.Quote(predicate) } )); }
      
      





このメソッドは、IQueryable <TSource>の新しいインスタンスを作成し、式をCreateQuery <TSource>()に渡します。ここで、引数として渡された述語を持つwhere()メソッド自体の呼び出しがsource.Expressionから元の式に追加されます。



したがって、先ほど作成したISimpleQueryable <T>とIQueryable <T>およびIQueryProvider <T>インターフェースは多少異なりますが、それらをLINQで使用するための原則は同じです:クエリに追加される各拡張メソッドは、それ自体に関する情報で式ツリーを補完し、次に、CreateQuery <T>()メソッドを使用してIQueryable <T>の新しいインスタンスを作成し、さらに集約メソッドでExecute <T>()メソッドを呼び出してクエリの実行を開始します。



LINQプロバイダーの開発に関するいくつかの言葉



LINQクエリ構築メカニズムは既に.Netに実装されているため、ほとんどの場合、LINQプロバイダーの開発はExecute()およびExecute <TResult>()メソッドの実装に帰着します。 ここで、実行されるようになった式ツリーを解析し、データソースの言語に変換し、クエリを実行し、結果をC#オブジェクトでラップして返す必要があります。 残念ながら、この手順にはかなりの数の異なるニュアンスの処理が含まれます。 さらに、LINQプロバイダーの開発に関する利用可能な情報はかなり少ないです。 著者によると、このトピックに関する記事は次のとおりです。



この記事の資料が、LINQプロバイダーがリモートデータソースでどのように機能するかを理解したい、またはそのようなプロバイダーの作成を開始したいが、まだ決定していない人にとって役立つことを願っています



All Articles