データ ポイント

WCF サービスにおける LINQ プロジェクションのクエリと代替策

Julie Lerman

サンプル コードのダウンロード

Julie Lerman先月、地元の .NET ユーザー グループのプレゼンターがセッション中にLINQ クエリを記述していたときに、私は、「LINQ なしでいったいどうやって生きながらえてきたのかしら。」とたずねると、「見当もつかないよ。」と彼は答えました。

確かにそのとおりです。LINQ は、Visual Studio 2008 で導入されてから、Microsoft .NET Framework でのコード記述方法に "それほど" の影響を与えています。これを Visual Basic と Visual C# で導入された多数の新しい言語機能と組み合わせると、メモリ内オブジェクトとメモリ内データ ソースへのクエリに関連する問題が一貫して解決されます。

LINQ の機能の 1 つ (これは、ありがたくもあり、ときにストレスの原因となることもあります) は、ランダム形式のデータを匿名型にプロジェクション (射影) できることです。単にこのようなランダム形式のデータの特殊なビューを取得する必要があるときは、使い捨てにされるであろう型のために新しいクラスを宣言することなく、匿名型を使用することが最適な解決策です。LINQ プロジェクションと匿名型によって甘やかされているのは確かです。では、ストレスの原因にもなり得ると言ったのはなぜでしょう。

データを別のメソッドに返す必要があるメソッドで LINQ プロジェクションを使用したことがある方、または Windows Communication Foundation (WCF) サービスの操作で LINQ プロジェクションを使用したことがある方は、おわかりかもしれません。

匿名型は使い捨てにされる型なので、宣言がなく、作成されたメソッド内でしか認識されないためです。匿名型の一覧を返すクエリを記述する場合、"匿名型の~" を表す方法がないので、"~の一覧を返す" ことを示すメソッド引数を定義する方法がありません。

単純なプロジェクションを使用する LINQ to Entities クエリを次に示します。

var custQuery = from c in context.Customers

                 select new {c.CustomerID, Name=c.LastName.Trim() + 

                 ", " + c.FirstName};

実行時、custQuery 変数は、実際には ObjectQuery<<>f__AnonymousType0<int,string>> となります。

var を使用すると、この非型を表す方法がなくても (またはこの方法を必要とすることなく) 処理を実行できます (Visual Basic では、var の代わりに Dim を使用します)。

メソッドからこのクエリの結果を返す場合、妥当な解決策は返す型を表すクラスを作成することだけです。しかし、これを行うと、匿名型の長所が消えてしまいます。そのためには、多くのコードを記述して、クラスを定義し、(場合によっては) 新しいクラスを収容する新しいプロジェクトも定義して、これらのクラスを使用するさまざまなアセンブリからこれらのクラスにアクセスできるようにしなければなりません。

最近まで、データ サービスが原因で、さらに難しい問題が発生していました。データのプロジェクションを行うために、サービスでカスタム操作を作成し、独自のクエリを実行してから、クライアントが理解する定義済みのクラスのなんらかの型を返す必要がありました。

サービスを操作しているとき、ネットワーク上を多くの型が行き来するコストを負担することなく、データの特定のビューを操作したいというシナリオは数多くあります。

結論を言えば、ドメインで余分な型を作成してこの一時的な欲求を満たす以外にも、選択肢があります。

WCF Data Services の新しいプロジェクション (射影) 機能

.NET Framework 3.5 SP1 の Data Services 更新プログラムには、WCF Data Services 用にいくつかの強力な機能が導入されています。これは、.NET Framework 4 にも含まれています。こうした機能の中に、データ サービスに対するクエリでプロジェクション (射影) を使用する機能があります。この更新プログラムに含まれるすべての新機能の詳細については、WCF Data Services チームのブログ記事 (blogs.msdn.com/astoriateam/archive/2010/01/27/data-services-update-for-net-3-5-sp1-available-for-download.aspx、英語) を参照することを強くお勧めします。

データ サービスの URI 構文に、$select 演算子が追加されています。この演算子により、プロパティのプロジェクションだけでなく、ナビゲーション プロパティのプロジェクションも可能になります。

顧客のいくつかのスカラー プロパティと共に、SalesOrderHeaders ナビゲーション プロパティを取得する、プロジェクションの簡単な例を次に示します。

http://localhost /DataService.svc/Customers(609)
  $select=CustomerID,LastName,FirstName,SalesOrderHeaders&$expand=
  SalesOrderHeaders

expand 演算子によって、注文へのリンクだけでなく、各注文のデータも強制的に結果に含められます。

図 1 に、このクエリの結果を示します。拡張された SalesOrderHeaders (1 つの注文のみが含まれています) を黄色で強調表示し、顧客情報を緑色で強調表示しています。

Figure 1 Results of a Data Services Query Projection Requesting Three Customer Properties and the Customer’s SalesOrder-Headers
図 1 顧客の 3 つのプロパティと、顧客の SalesOrderHeaders を要求する、データ サービスのクエリ プロジェクションの結果

.NET Framework の LINQ to REST 機能と、WCF Data Services 用の Silverlight クライアント API が更新され、同様にプロジェクションが可能になっています。

var projectedCust = (from c in context.Customers

                    where c.CustomerID==609

                    select new {c.CustomerID, c.LastName})

                    .FirstOrDefault();

ProjectedCust は、クライアント アプリケーションで使用できる匿名型になっています。

既知のエンティティ型にプロジェクションを行うこともできます。場合によっては、クライアントで行われた変更を DataContext で追跡し、サービスの SaveChanges メソッドを通じて、これらの変更を保存して戻すこともできます。値が指定されていないすべてのプロパティには既定値 (または null を使用できる場合は null) を設定し、これらがデータベースに保存されるようにします。

プロジェクションが行われた厳密型を EDM から有効にする

Entity Framework のエンティティ データ モデル (EDM) を使用している場合、匿名型を作成したメソッドからその匿名型を渡す必要があるときに、匿名型の扱いに悩まされないようにする便利な方法があります。

EDM には、QueryView というマッピングがあります。以前、データ サービスでプロジェクションがサポートされる前は、多くのクライアントにこのことを指摘していました。この方法では、データ サービスの問題だけでなく、カスタム WCF サービスと WCF RIA Services の問題もうまく解決されます。

QueryView とは、Entity Framework メタデータにおける特殊な型のマッピングです。一般に、エンティティのプロパティがメタデータのストア モデル (ストア スキーマ定義言語 (SSDL)) で表されているときは、エンティティのプロパティをデータベースのテーブルか、ビューの列にマップします (図 2 参照)。

Figure 2 Mapping Table Columns Directly to Entity Properties
図 2 テーブルの列をエンティティのプロパティに直接マッピングする

ただし、QueryView では、SSDL で表されるそれらのテーブル列に直接マップするのではなく、それらのテーブル列を含むビューを作成できます。QueryView を使用する理由はたくさんあります。たとえば、エンティティを読み取り専用で公開する、条件付きマッピングでは許可されない方法でエンティティをフィルター処理する、データベースのデータ テーブルのさまざまなビューを提供するといったことが挙げられます。

アプリケーションでプロジェクションを行うことが多い匿名型の代替策として私が注目するのは、この 3 つの目的のうちの最後です。1 つの例として、選択一覧があります。たとえば、ID と顧客の名前だけが必要なドロップダウンに顧客の型全体を返す必要があるでしょうか。

QueryView を構築する

QueryView を作成する前に、モデル内でエンティティを作成する必要があります。このエンティティは、目的のビューの形状を表します (CustomerNameAndID エンティティなど)。

しかし、このエンティティを SSDL で Customer テーブルに直接マップすることはできません。Customer エンティティと CustomerNameAndID エンティティの両方を、テーブルの CustomerID 列にマップすると競合が発生します。

代わりに、データベースでテーブルのビューを作成するのと同じように、メタデータで直接 SSDL 形式の Customer ビューを作成できます。QueryView は、文字どおり SSDL による Entity SQL 式です。これは、モデルのマッピング スキーマ言語 (MSL) のメタデータの一部です。デザイナーでの QueryView の作成がサポートされていないため、XML を直接入力する必要があります。

テーブルのストア スキーマにマップすることになるため、どのようになるかを確認しておくことをお勧めします。図 3 に、Customer データベース テーブルを SSDL で記述したコードを示します。これは、プロバイダーのデータ型が使用されている点は除いて、概念モデルのメタデータにおける Customer エンティティと同じように見えます。

図 3 データベースの Customer テーブルを SSDL で記述したコード

<EntityType Name="Customer">

  <Key>

    <PropertyRef Name="CustomerID" />

  </Key>

  <Property Name="CustomerID" Type="int" Nullable="false"

            StoreGeneratedPattern="Identity" />

  <Property Name="Title" Type="nvarchar" MaxLength="8" />

  <Property Name="FirstName" Type="nvarchar" Nullable="false" 

            MaxLength="50" />

  <Property Name="MiddleName" Type="nvarchar" MaxLength="50" />

  <Property Name="LastName" Type="nvarchar" Nullable="false" 

            MaxLength="50" />

  <Property Name="Suffix" Type="nvarchar" MaxLength="10" />

  <Property Name="CompanyName" Type="nvarchar" MaxLength="128" />

  <Property Name="SalesPerson" Type="nvarchar" MaxLength="256" />

  <Property Name="EmailAddress" Type="nvarchar" MaxLength="50" />

  <Property Name="Phone" Type="nvarchar" MaxLength="25" />

  <Property Name="ModifiedDate" Type="datetime" Nullable="false" />

  <Property Name="TimeStamp" Type="timestamp" Nullable="false"

            StoreGeneratedPattern="Computed" />

</EntityType>

QueryView のもう 1 つの重要な要素は、ストア スキーマの名前空間 ModelStoreContainer でしょう。これで QueryView の式を構築するのに必要な要素を用意できました。モデルで作成した CustomerNameAndID エンティティに、必要な 3 つのフィールドを SSDL からプロジェクションを行う QueryView を次に示します。

SELECT VALUE AWModel.CustomerNameAndID(c.CustomerID, c.FirstName, 

        c.LastName) FROM ModelStoreContainer.Customer as c

この Entity SQL を日本語に訳すと、「ストア スキーマの Customer に照会し、これらの 3 つの列を抽出してから、これらを CustomerNameAndID エンティティとして返します」となります。AWModel は、概念モデルのエンティティ コンテナーの名前空間です。式で参照されている、概念スキーマ定義言語 (CSDL) 型と SSDL 型両方の厳密に型指定された名前を使用する必要があります。

プロジェクションの結果 (整数値、文字列、および文字列) が対象エンティティのスキーマと一致していればマッピングは成功です。プロジェクション内で関数と連結の使用を試みました。たとえば、(c.CustomerID, c.FirstName + c.LastName) などです。しかし、関数は使用できないことを示すエラーが表示されて失敗しました。そのため、FirstName プロパティと LastName プロパティを使用して、クライアントで連結を行わなければなりませんでした。

QueryView をメタデータに配置する

QueryView の式をエンティティの EntitySetMapping 要素内に配置して、メタデータの EntityContainerMapping 内に配置されるようにする必要があります。図 4 に、何も手を加えていない EDMX ファイルの XML に表示される、この QueryView を示します (黄色で強調表示しています)。

Figure 4 A QueryView in the Mappings Section

図 4 Mapping セクションの QueryView

CustomerNameAndID がモデルに含まれるようになったので、すべてのコンシューマーから使用できるようになります。QueryView には、もう 1 つメリットがあります。この QueryView の目的は読み取り専用の参照一覧を作成することですが、QueryView を使用してマップされているエンティティを更新することもできます。コンテキストは CustomerNameAndID オブジェクトへの変更を追跡します。Entity Framework では、このエンティティ向けに挿入、更新、および削除のコマンドを自動生成することはできませんが、ストアド プロシージャをマップすることができます。

QueryView のメリットを活用する

モデルに QueryView が含まれるようになったので、プロジェクションや匿名型を使用して、これらのデータのビューを取得する必要がなくなります。WCF Data Services では、CustomerNameAndIDs がクエリ対象の有効なエンティティ セットになります (以下を参照)。

List<CustomerNameAndID> custPickList = 

  context.CustomerNameAndIDs.ToList();

厄介なプロジェクションは使用されていません。できれば、カスタム WCF サービスにサービス操作を作成すると、アプリケーションやプロジェクトで新しい型を定義することなく、この厳密に型指定されたオブジェクトをサービス操作に返すことができるようになります。

public List<CustomerNameAndID> GetCustomerPickList()

    {

      using (var context = new AWEntities())

      {

        return context.CustomerNameAndIDs.OrderBy(

          c => c.LastName).ToList();

      }

    }

QueryView には姓名を連結できないという制限があるため、最後にこの連結を行うことは、サービスを使用する開発者の仕事です。

WCF RIA Services でも QueryView の恩恵を受けることができます。メソッドを公開して、ドメイン サービスからレストランの選択一覧を取得する場合は、ドメイン サービスに新たなクラスを作成してプロジェクションが行われるプロパティを表さなくても、モデル内の QueryView によってこの RestaurantPickList エンティティがサポートされるため、こうしたデータを簡単に提供することができます。

public IQueryable<RestaurantPickList> GetRestaurantPickList()

    {

      return context.RestaurantPickLists;

    }

QueryView またはプロジェクション、備えは万全

さまざまなデータ型を含むビューのプロジェクションを行う機能は、クエリを行う際に大きなメリットとなります。これは、WCF Data Services への優れた追加機能です。それでも、このようなビューにアクセスする際に、プロジェクションを行う必要がなかったり、結果の共有について心配したりすることがなければ、コーディングの作業がいくらか単純になるでしょう。

最後に、.NET Framework 4 の Entity Framework で外部キーを使用すれば、読み取り専用のエンティティを返して、編集中のエンティティでプロパティを使用して外部キーのプロパティを更新するだけでよくなるため、QueryView の選択一覧がさらにわかりやすくなります。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女が執筆した『Programming Entity Framework』 (O'Reilly Media、2009 年) は絶賛を浴びました。彼女には Twitter (julielerman、英語) から連絡できます。

この記事のレビューに協力してくれた技術スタッフの Alex James に心より感謝いたします。