// Get the place name(s) to query the Web service with.
LocationFinder lf = new LocationFinder(lambdaExpression.Body);
List<string> locations = lf.Locations;
if (locations.Count == 0)
throw new InvalidQueryException("You must specify at least one place name in your query.");
// Call the Web service and get the results.
Place[] places = WebServiceHelper.GetPlacesFromTerraServer(locations);
// Copy the IEnumerable places to an IQueryable.
IQueryable<Place> queryablePlaces = places.AsQueryable<Place>();
結果のIQueryableは、もはやTerraServer LINQプロバイダーの手を離れ、LINQ to ObjectsのLINQプロバイダーが処理を行うようになる。
まだ終わりではない
最初にInnerMostWhereFinderクラスで、最も内側のWhereを探索した事を覚えているだろうか? いま、一息ついたところだが、ここまでの処理で実現したのは、その「最も内側のWhere条件」だ。その外側のLINQクエリは放置されている。これを処理しなければならない。
しかし、前述のようにもうTerraServerは関係ないため、配列をAsQueryableした結果を使って続きのクエリを実行させる必要がある。これを行わせるのが、「ExpressionTreeModifier」クラスだ。このクラスもExpressionVisitorを継承している。やっていることは単純だ。TerraServerのコンテキストクラスのインスタンスを、上記配列をAsQueryableしたものに挿げ替える。
生成されたExpressionは、もはやTerraServerコンテキストを含まない。かつ、サービスから得られた結果を配列で含んでいるため、Expressionツリー全体がLINQ to Objectsで評価可能な状態にある。CreateQueryを呼び出してIQueryableを生成すれば、そのインスタンスはLINQ to ObjectsのLINQプロバイダーが管理する。
こうして、LINQ to TerraServerが完成した。
// コンテキストを生成
var terraPlaces = new QueryableTerraServerData<Place>();
// コンテキストに検索条件を与え、TerraServerサービスにクエリを発行し、結果を取得する
var query =
from place in terraPlaces
where place.Name == "Johannesburg"
select place;
using (var context = new AdventureWorksLT2012_DataEntities())
{
// 検索条件を変数で与える
var selectType = "Shipping";
var addresses =
from customerAddress in context.CustomerAddress
where customerAddress.AddressType == selectType
select customerAddress;
}
using (var context = new AdventureWorksLT2012_DataEntities())
{
// 検索条件をクエリの式中に直接埋め込む
var addresses =
from customerAddress in context.CustomerAddress
where customerAddress.AddressType == "Shipping"
select customerAddress;
}
var r = new Random();
// 乱数から、5で割り切れる数を抽出
var div5 =
from index in Enumerable.Range(0, 100000)
let value = r.Next()
where (value % 5) == 0
select value;
// 更に3で割り切れる数を抽出
var div5and3 =
from value in div5
where (value % 3) == 0
select value;
これは「人類の英知」によって、以下のクエリに変形できる。
// 乱数から、5と3で割り切れる数を抽出
var div5and3 =
from index in Enumerable.Range(0, 100000)
let value = r.Next()
where ((value % 5) == 0) && ((value % 3) == 0)
select value;
// LINQ to Entitiesによるクエリの例
using (var context = new AdventureWorksLT2012_DataEntities())
{
var addresses =
from customerAddress in context.CustomerAddress
where customerAddress.AddressType == "Shipping"
select customerAddress;
}
もう何度も出ている例だが、実はまだこれをメソッド形式で例示していない。
// メソッド形式に変更
using (var context = new AdventureWorksLT2012_DataEntities())
{
var addresses =
context.CustomerAddress.
Where(customerAddress => customerAddress.AddressType == "Shipping");
}
「TerraServer-USA Web Services」というサービスが存在したとして(実際あるようだが、何故か応答が返ってこない)、そのウェブサービスに対してクエリを発行し、結果を取得出来る。何もない状態では、WCFで定義されたインターフェイスに従って、メソッド呼び出しのような形式でサービスを利用するが、このサービスに対応するLINQプロバイダを書けば、LINQクエリの記述でサービスを利用する事が可能になる。つまり、既存のWCFインターフェイスを使用して、「LINQ to TerraServer」を作ると言う事になる。
全体的な構造
LINQクエリを記述する側は、IEnumerableやIQueryableといったインターフェイスにだけ注目する。LINQプロバイダーを作る場合(そして、LINQ to SQLやLINQ to Entities)は、IQueryableインターフェイスを実装した独自のクラスを用意する。と同時に、LINQプロバイダーとなる「IQueryProvider」インターフェイスを実装したクラスも用意する。このクラスが、LINQクエリーのExpressionを解釈し、SQL文を生成したりする。今回はWCFを使用して、TerraServerのサービスにアクセスする。
また、LINQクエリの記述を成立させるには、結果を格納するエンティティとなるクラスが必要だ。例えば、
using (var context = new AdventureWorksLT2012_DataEntities())
{
// CustomerAddressエンティティクラスが受け皿となるクエリ
IQueryable<CustomerAddress> persons =
from customerAddress in context.CustomerAddress
where customerAddress.AddressType == "Shipping"
select customerAddress;
}
このようなクエリが(RDBやWCFを通じてサーバーで)実行され、結果が返される。その結果をこのクラス(CustomerAddress)に格納する。そのため、クラスの定義が必要となる。
もう一つ、LINQ to SQLやLINQ to Entitiesでは、「コンテキスト」と呼ばれるRDBへの接続を管理するクラスも必要だ。これがないと、LINQソースとなる端点(上記の例であれば、context.CustomerAddress)が提供出来ないので、クエリが書けない。但し、別の方法もある。普通の列挙子をAsQueryableでIQueryableに変換した場合は、コンテキストとなるクラスは存在しない。つまり、何らかの方法で、LINQソースとなる端点が提供できれば良いと言う事だ。
バイナリのカバレッジデータを変換するMSBuildタスクのプラグイン「VS Coverage to XML Converter MSBuild task」は、VS2008に対応したコードだ。調査を続けると、どうもカバレッジデータを読み取るDLLが、Visual Studioのバージョン毎に違っている。おまけに、VS2010からは大幅に仕様が変更されており、ファイルが全く認識されない。
「VS Coverage to XML Converter MSBuild task」のソースコードを見ると、ライブラリを呼び出してバイナリデータをXMLに変換している。何のことは無い、バイナリデータがDataSet(インメモリのテーブルコレクション)に変換されるので、DataSet.WriteXmlでデータセット形式のXMLで書き出しているだけだ。
// queryableの列挙子を取得する擬似コード
public static IEnumerable<T> PseudoAsEnumerable<T>(this IQueryable<T> queryable)
{
// queryableで示されるクエリを実行し、結果群が返される(GetEnumeratorの呼び出し)
foreach (var value in queryable)
{
yield return value;
}
}
queryableは普通の列挙子ではない。LINQ to SQLやLINQ to Entitiesで示される、何らかのストレージとのインターフェイスを抽象化したものだ。そのため、このforeachによって何が引き起こされるのかは、queryableが何を対象にしているのかによって異なる。LINQ to SQLやLINQ to Entitiesでは、RDBに対してSQL文が発行され、結果が返って来るだろう。そのレコード群がforeachで列挙される。
では1番目の変換はどうだろう。
randomDatasはLINQ to Objectsのクエリ式だ。IQueryableで表現するためには、このクエリ全体が「記述的」になる必要がある。つまり、randomDatasクエリ式を「呼び出す式」がIQueryableで表現される。
IQueryableがGetEnumeratorによって列挙される時、randomDatasクエリ式を解釈する「LINQプロバイダー」が動作し、プロバイダーがrandomDatasクエリ式を列挙する。
当然ながら、この例ではRDBにアクセスするわけではないので、queryableIntDatasは「SQL文」を表している訳ではない。以下はデバッガのスナップショットだ。
IQueryableインターフェイスには、「Expression」プロパティが存在する。このプロパティは、例の「Expression」だ。IQueryableが表現するクエリ式全体が、このExpressionプロパティから公開される。そこで、このExpressionがどうなっているのか、Expressionの探索で示したFlatDumpExpressionVisitorを使ってXMLにダンプすると、以下のような単一のノードが返される。
結局、IQueryableが指す参照先は、IEnumerableのベールに包まれたrandomDatasであり、固定値(Constant)として認識されている。同じIQueryableを基礎としているのに、これは明らかにLINQ to SQLやLINQ to Entitiesとは異なる。
この違いは、LINQソースから生まれる。LINQ to Entitiesの例では、AdventureWorksLT2012_DataEntitiesクラスのCustomerプロパティからクエリを開始している。AsQueryableは、LINQ to Objectsのクエリを変換する。この違いによって、IQueryableの評価を受け持つ「LINQプロバイダー」が異なり、SQL文に変換されたり、列挙子の呼び出しに変換されたりする。