データ ポイント
Entity Framework 手法の解明: 関連データの読み込み
Julie Lerman
先月の「データ ポイント」コラムでは、Database First、Model First、および Code First の各オプションからモデリング ワークフロー手法を選択する基準について概説しました。今月は、選択が必要な別の重要なオプションとして、データベースから関連データを取得する方法を取り上げます。選択可能なオプションには、一括読み込み、明示的読み込み、遅延読み込み、またはクエリ プロジェクションがあります。
ただし、アプリケーションのさまざまなシナリオに応じてさまざまなデータ読み込み手法が必要になるため、この決定は 1 回下せば終わりではありません。そのため、作業に適した手法を選択できるよう、各手法を理解しておくことをお勧めします。
たとえば、家族のペットの記録を追跡するアプリケーションがあるとします。モデルには Family クラスと Pet クラスがあり、Family クラスと Pet クラスの間には一対多のリレーションシップがあります。この状況で、家族とそのペットについての情報を取得することを考えます。
来月のコラムでは、このシリーズの続きとして、LINQ to Entities、Entity SQL、および各オプションの変化形を使用して Entity Framework にクエリを実行するさまざまな手法を紹介する予定です。しかし今月は、すべての例で LINQ to Entities だけを使用します。
データベースとの 1 回のやり取りで一括読み込みする
一括読み込みにより、1 回のやり取りでデータベースからすべてのデータを取得することができます。そのために、Entity Framework には Include メソッドが用意されています。Include メソッドは、関連データへのナビゲーション パスを表す文字列を受け取ります。Family と各 Family の Pets のコレクションがそれぞれ含まれたグラフを返す Include メソッドの例を次に示します。
from f in context.Families.Include("Pets") select f
モデルに VetVisit という別のエンティティがあり、このエンティティと Pet の間に一対多のリレーションシップがある場合、次のように、家族、家族のペット、およびそのペットの獣医受診記録を一括で取得できます。
from f in context.Families.Include("Pets.VetVisits") select f
一括読み込みの結果は、オブジェクト グラフとして返されます (図 1 参照)。
図 1 一括読み込みクエリから返されるオブジェクト グラフ
ID | Name | Pets | |||||
ID | Name | Type | VetVisits | ||||
2 | LermanJ | 2 | Sampson | Dog | 1 | 2/1/2011 | Excellent |
5 | 4/1/2011 | Nail Clipping | |||||
4 | Sissy | Cat | 1 | 3/2/2011 | Excellent | ||
3 | GeigerA | 3 | Pokey | Turtle | 3 | 2/5/2011 | Excellent |
4 | Riki | Cat | 6 | 4/8/2011 | Excellent |
Include メソッドは非常に柔軟です。同時に複数のナビゲーション パスを使用できるうえ、親エンティティに移動したり、多対多のリレーションシップを使用して移動したりできます。
Include メソッドによる一括読み込みはとても便利ですが、使いすぎると (1 つのクエリに多数の Include メソッドを含めたり、1 つの Include メソッドに多数のナビゲーション パスを含めたりすると)、クエリのパフォーマンスが急激に低下します。Entity Framework で構築されたそのままのクエリでは多数の結合が使用されます。また、要求したグラフを返せるように、データベースの結果の形状が必要以上に複雑になることや、必要以上に多くの結果が返されることがあります。ただし、Include メソッドを避けるべきだと言っているわけではありません。Entity Framework クエリのプロファイリングを行って、パフォーマンスの低いクエリを生成しないようにします。そのままのクエリのパフォーマンスが著しく低い場合は、アプリケーションのそのクエリを生成する領域についてクエリ手法を再考すべきです。
データベースとのやり取りを増やし遅延読み込みする
データを取得する場合、関連データがすぐに必要ではないことや、すべての結果に関連データを必要としないこともあります。たとえば、アプリケーションで全家族を取得する必要があっても、ペットの情報が必要なのは一部の家族だけだとしましょう。この場合、Include メソッドで全家族のペットすべてを一括読み込みする意味は、おそらくありません。
Entity Framework には、後から関連データを読み込む方法が 2 つあります。1 つは遅延読み込みと呼ばれ、適切に設定すれば自動的に実行されます。
遅延読み込みでは、関連データを参照するだけで、Entity Framework によってメモリに関連データが読み込まれているかどうかが確認されます。読み込まれていなければ、Entity Framework が自動的にクエリを作成および実行し、関連データを読み込みます。
たとえば次のように、いくつかの Family オブジェクトを取得するクエリを実行し、取得した Family オブジェクトの 1 つの Pets を取得するよう Pets プロパティを指定することによって Entity Framework をトリガーすると、Entity Framework がその Pets を取得します。
var theFamilies= context.Families.ToList();
var petsForOneFamily = theFamilies[0].Pets;
Entity Framework では、遅延読み込みの有効と無効は ObjectContext の ContextOptions.LazyLoadingEnabled プロパティを使用して切り替えます。既定では、新しく作成したモデルの LazyLoadingEnabled プロパティが true になるよう Visual Studio で定義されるため、新しいモデルでは遅延読み込みが既定で有効になります。
コンテキストのインスタンス作成時に既定で遅延読み込みを有効にすると、アプリケーションにとって非常に便利な場合がありますが、この動作を把握していない開発者にとっては問題になることもあります。開発者は、気付かないうちにデータベースとのやり取りを増やしている可能性があります。遅延読み込みを使用しているかどうか把握するのは開発者の役割で、必要に応じて、遅延読み込みを使用するかどうかを明示的に選択できます。そのためには、コードで LazyLoadingEnabled プロパティを true または false に設定して、有効と無効を切り替えます。
遅延読み込みを行うのは EntityCollection クラスと EntityReference クラスなので、Plain Old CLR Object (POCO) クラスを使用している場合は、LazyLoadingEnabled プロパティが true でも、既定では遅延読み込みを使用できません。ただし、ナビゲーション プロパティを virtual または Overridable としてマークすると、Entity Framework の動的プロキシの動作がトリガーされ、POCO を一括読み込みできるようにする実行時のプロキシが作成されます。
遅延読み込みは Entity Framework で使用できるすばらしい機能ですが、この機能が役立つのは、遅延読み込みが有効になる条件を把握していて、遅延読み込みが適切な状況と不適切な状況を把握している場合だけです。たとえば、MSDN マガジンの記事「Entity Framework を使用した SQL Azure のネットワーク待機時間の短縮」(msdn.microsoft.com/magazine/gg309181) では、社内設置型のサーバーからクラウド データベースに対して遅延読み込みを使用する場合のパフォーマンスへの影響を取り上げています。Entity Framework によって実行されるデータベース クエリのプロファイリングは、読み込み手法を選択する際に役立つ重要な部分です。
また、遅延読み込みが無効な場合を把握しておくことも重要です。LazyLoadingEnabled プロパティを false に設定すると、先ほどの例の theFamilies[0].Pets を実行してもデータベース クエリがトリガーされず、データベースにデータが存在していても、その家族にペットがいないと通知されます。したがって、遅延読み込みを使用する場合は、遅延読み込みが有効になっていることを確認してください。
データベースとのやり取りを増やし明示的に読み込む
遅延読み込みを無効にして、データを読み込むタイミングをもっと明示的に制御することもできます。Include メソッドで明示的に読み込む以外にも、Entity Framework ではいずれかの Load メソッドを使用し、関連データを選択して明示的に取得できます。
既定のコード生成テンプレートを使用してエンティティ クラスを生成すると、クラスは EntityObject から継承され、関連データは EntityCollection 型か EntityReference 型で公開されます。どちらの型にも Load メソッドがあり、このメソッドを呼び出すと Entity Framework が関連データを取得します。Pets オブジェクトの EntityCollection の例を次に示します。Load メソッドには戻り値がないことに注意してください。
var theFamilies = context.Families.ToList();
theFamilies[0].Pets.Load();
var petsForOneFamily = theFamilies[0].Pets;
Entity Framework で関連プロパティ (Family の Pets コレクション) を読み込むクエリが作成および実行されると、Pets を操作できるようになります。
Load メソッドで明示的に読み込む 2 つ目の方法は、EntityCollection や EntityReference ではなく ObjectContext から呼び出すことです。Entity Framework の POCO のサポートを利用している場合、ナビゲーション プロパティが EntityCollection 型や EntityReference 型にならないため、Load メソッドを使用できません。代わりに、ObjectContext.LoadProperty メソッドを使用できます。LoadProperty メソッドでは、ジェネリックを使用して読み込み元の型を特定し、ラムダ式を使用して、読み込むナビゲーション プロパティを指定します。LoadProperty メソッドを使用して特定の家族のインスタンスの Pets を取得する例を次に示します。
context.LoadProperty<Family>(familyInstance, f => f.Pets)
読み込む代わりにクエリ プロジェクションを実行する
忘れてはならないのが、クエリでもプロジェクションを使用できることです。たとえば次のように、エンティティを取得するクエリを作成し、取得する関連データをフィルター処理できます。
var famsAndPets=from family in context.Families
select new {family,Pets=family.Pets.Any(p=>p.Type=="Reptile")};
このクエリでは、データベースと 1 回やり取りするだけで、全家族と、は虫類を飼っている家族のすべてのペットを返します。ただし、famsAndPets クエリでは、家族とそのペットのグラフではなく、1 つのプロパティが Family を表し、別のプロパティが Pets を表す一連の匿名型を返します (図 2 参照)。
図 2 Family プロパティと Pets プロパティがあるプロジェクションされた匿名型
Family | Pets | |||
ID | Name | ID | Name | Type |
2 | LermanJ | |||
3 | GeigerA | 3 | Pokey | Turtle |
4 | Riki | Cat |
長所と短所を評価する
ここまで、関連データの取得に使用できる手法を 4 つ紹介しました。アプリケーションでこれらの手法を同時に使用することもできます。アプリケーションに含まれる多様なシナリオに応じて、このようなさまざまな機能それぞれに採用する理由が見つかります。それぞれのシナリオに適した手法を選択する前に、各手法の長所と短所を検討します。
クエリ対象の中核データすべての関連データが必要になることが事前にわかっているシナリオでは、Include メソッドで一括読み込みすると便利です。ただし、2 つの短所があるので注意が必要です。Include メソッドやナビゲーション パスが多すぎると、Entity Framework でパフォーマンスの低いクエリが生成されることがあります。また、Include メソッドを使用するとコーディングが容易になるため、必要以上に多くの関連データを返しているかどうか注意します。
遅延読み込みでは、便利なことに、関連データについてコードで言及するだけで自動的に関連データを取得できます。コーディングは簡単ですが、データベースとやり取りする回数に注意が必要です。1 ~ 2 回のやり取りで十分なのに、40 回もやり取りしていることがあります。
明示的読み込みでは、関連データが読み込まれるタイミング (および読み込まれるデータ) をきめ細かく制御できます。ただし、このオプションを把握していないと、データベースに存在する関連データの有無について、アプリケーションに誤った情報が通知されることがあります。多くの開発者は明示的読み込みを面倒だと感じていますが、きめ細かく管理できることに満足している開発者もいます。
クエリでプロジェクションを使用すると、単一のクエリで関連データを選択して取得できるので、両方の長所を利用できることがあります。しかし、クエリ プロジェクションで匿名型を返す場合、Entity Framework の状態マネージャーでオブジェクトが追跡されず、オブジェクトを更新できないので、操作が面倒になることがあります。
図 3 に、最初に手法を選択する際に使用できる意思決定フローチャートを示します。ただし、パフォーマンスも考慮すべきです。クエリのプロファイリング用ツールやパフォーマンス テスト用ツールを利用して、適切なデータ読み込み手法を選択していることを確認してください。
図 3 最初に読み込み手法を決定する方法
Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女が執筆した『Programming Entity Framework』(O'Reilly Media、2010 年) は絶賛を浴びました。Twitter (twitter.com/julielerman、英語) で彼女をフォローしてください。
この記事のレビューに協力してくれた技術スタッフの Tim Laverty に心より感謝いたします。