ASP.NET MVCアプリケーションの関連データを読み取る
これは、Microsoftの公式チュートリアルGetting Entity Framework 6のコードを最初にMVC 5シリーズを使用して翻訳したもので、7番目の記事は、ASP.NET MVCアプリケーションの関連データの読み取りです。
原文:ASP.NET MVCアプリケーションのエンティティフレームワークで関連データを読み取る
前のチュートリアルでは、学校のデータモデルを完成させました。このチュートリアルでは、現実に関連するデータを読み取る方法を学習します。これは、Entity Frameworkのナビゲーションプロパティを介して読み込まれたデータを指します。
以下のスクリーンショットは、完了しようとしているページを示しています。
遅延、事前ロード、および現実的なロード関連データ
エンティティフレームワークには、エンティティのナビゲーションプロパティから関連データをロードするためのさまざまなメソッドがあります。
- 遅延読み込み
。エンティティが初めて読み込まれるとき、関連するデータは取得されません。ただし、最初にナビゲーションプロパティにアクセスしようとすると、ナビゲーションプロパティに必要なデータが自動的に読み込まれます。結果は、エンティティ自体を読み取り、次に各関連エンティティを読み取るために、複数のクエリを使用してデータベースに送信されます。DbContextクラスはデフォルトで遅延読み込みを使用します。
- 熱心な
読み込み。エンティティが読み取ると、エンティティに関連するデータがすぐに取得されます。これにより、通常、必要なすべてのデータが1つの接続クエリで取得されます。Includeメソッドを使用してプリロードを指定できます。
- 明示的な読み込み
。コードで関連データを明示的に取得することを除いて、遅延読み込みに少し似ています。ナビゲーションプロパティにアクセスしても、自動的には読み込まれません。エンティティのオブジェクト状態マネージャーを使用してコレクションのCollection.Loadメソッドを呼び出すか、単一のエンティティのプロパティを保持するReference.Loadメソッドを使用して、関連データを手動で読み込む必要があります。(次の例では、管理者ナビゲーションプロパティをロードする場合、参照(x
=> x.Administrator)を使用してコレクション(x => x.Courses)を置き換える必要があります)
遅延読み込みも明示的読み込みも属性の値をすぐに取得しないため、遅延読み込みとも呼ばれます。
パフォーマンスに関する考慮事項
各エンティティに関連するデータがすぐに必要であることがわかっている場合は、通常、プリロードが最高のパフォーマンスを提供します。単一のクエリをデータベースに送信してデータを一度にフェッチする効率は、通常、各エンティティで別のクエリを発行する効率よりも高いためです。たとえば、上記の例では、各学部に10の関連コースがあると仮定すると、プリロードの結果、データベースとのクエリは1つだけ(結合共同クエリ)になります。遅延読み込みと明示読み込みの両方で、11のクエリとラウンドトリップが発生します。レイテンシが長い場合、追加のクエリとラウンドトリップがしばしば不利になります。
一方、遅延読み込みの方が効率的な場合があります。プリロードにより、SQL Serverが効果的に処理できない非常に複雑な結合クエリが発生する可能性があります。または、アクセスする必要があるエンティティのナビゲーション属性を処理している場合、この属性はエンティティセットのサブセットにすぎません。プリロードはすべてのデータをロードするため、レイロードはプリロードよりも優れている場合があります。それらにアクセスする必要がない場合でも。アプリケーションのパフォーマンスが非常に重要である場合は、これらの2つの方法のどちらかをテストして、最適なものを選択します。
遅延読み込みは、パフォーマンスの問題を引き起こす一部のコードをブロックする可能性があります。たとえば、コードはプリロードまたは明示的なロードを指定していませんが、多数のエンティティを処理し、各反復でナビゲーション属性を使用すると、コードの効率が低くなる可能性があります(データベースラウンドトリップクエリが多数になるため)。開発環境で正常に動作するアプリケーションでは、Windows Azure SQLデータベースに移動する際の遅延が増加するため、遅延読み込みのパフォーマンスが低下する可能性があります。分析とテストを行って、遅延読み込みが適切であることを確認する必要があります。詳細については、「Entity Framework戦略の謎を解く:関連データの読み込み」および「Entity Frameworkを使用してSQL Azureへのネットワーク待機時間を短縮する」を参照してください。
シリアル化の前に遅延読み込みを無効にする
シリアル化中に遅延読み込みを有効にすると、予想よりも多くのデータをクエリする可能性があります。シリアル化は通常、クラスのすべての属性にアクセスします。属性アクセスは遅延ロードをトリガーし、遅延ロードされたエンティティもシリアル化されます。最終的には、属性の遅延読み込みとシリアル化がさらに発生する可能性があります。この連鎖反応を防ぐには、エンティティをシリアル化する前に遅延読み込みを無効にしてください。
シリアル化の問題を回避する1つの方法は、エンティティフレームワークでのWeb APIの使用チュートリアルに示されているように、エンティティオブジェクトではなくデータ転送オブジェクト(DTO)をシリアル化することです。
DTOを使用したくない場合は、遅延読み込みを無効にし、プロキシの作成を無効にしないでプロキシの問題を回避できます。
ここでは無効に遅延読み込みにいくつかの方法があります。
- 特定のナビゲーション属性については、仮想キーワード宣言を省略します。
- すべてのナビゲーションプロパティについて、LazyLoadingEnabledをfalseに設定し、次のコードをコンテキストクラスのコンストラクターに配置できます。
this.Configuration.LazyLoadingEnabled = false;
学部名を示すコースページを作成する
Courseエンティティには、コースに割り当てられたDepartmentエンティティを含むナビゲーション属性が含まれています。割り当てられた部門の名前をコースリストに表示するには、Course.DepartmentナビゲーションプロパティであるDepartmentエンティティからNameプロパティを取得する必要があります。
次の図に示すように、Courseエンティティタイプの新しい「ビュー付きMVC 5コントローラ(Entity Frameworkを使用)」コントローラを作成し、以前にStudentコントローラを作成したときと同じ設定を使用して、CourseControllerという名前を付けます。
コントローラーを開き、Indexメソッドを表示します。
public ActionResult Index()
{
var courses = db.Courses.Include(c => c.Department);
return View(courses.ToList());
}
自動生成されたスキャフォールディングがIncludeメソッドを使用して、Department属性のプリロードを指定していることがわかります。
Views \ Course \ Index.cshtmlを開き、オリジナルを次のコードに置き換えます。
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewBag.Title = "Courses";
}
<h2>Courses</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
Department
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.CourseID)
</td>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Credits)
</td>
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.CourseID }) |
@Html.ActionLink("Details", "Details", new { id=item.CourseID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.CourseID })
</td>
</tr>
}
</table>
足場コードに次の変更を加えました。
- タイトルを「索引」から「コース」に変更します。
- CourseID属性値を表示する数値行を追加しました。デフォルトでは、スキャフォールディングは主キーテンプレートを生成しません。これは通常、エンドユーザーにとって意味がないためです。ただし、この場合は、この方法で表示できることを示すためにのみ使用されます。
- コースの行を右に移動してタイトルを変更しました。スキャフォールディングにより、DepartmentエンティティのName属性が正しく選択されましたが、このコースページでは、列のタイトルはNameではなくDepartmentになっています。
部門の行では、足場コードは、部門エンティティのNameプロパティがナビゲーションプロパティを介して読み込まれることを示しています。
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
このページを実行(コースタブを選択)して、部門名のリストを表示します。
コースと登録情報を表示するインストラクターページを作成する
このセクションでは、コントローラーを作成し、講師エンティティビューを使用して講師ページを表示します。
このページでは、次の方法で関連データを読み取ります。
- インストラクターリストには、OfficeAssignmentエンティティの関連データが表示されます。インストラクターとOfficeAssignmentエンティティの間には1対1またはゼロの関係があり、OfficeAssignmentエンティティの量を使用してプリロードできます。前述のように、メインテーブルのすべての関連データが必要な場合は、プリロードがより効果的です。ここでは、すべての講師のオフィス割り当てを表示します。
- ユーザーが講師を選択すると、関連するコースエンティティが表示されます。インストラクターとコースエンティティの間には、多対多の関係があります。コースエンティティとそれに関連する部門エンティティでプリロードを使用することもできます。ただし、ここでは、選択したインストラクターのコース情報のみが必要なため、遅延読み込みの方が効果的です。実際、ここでは、遅延読み込みを使用してナビゲーションプロパティ間でナビゲーションプロパティをロードする方法を示します。
- ユーザーがコースを選択すると、登録されたエンティティレコードの関連データが表示されます。コースと登録エンティティは、1対多の関係にあります。Enrollmentエンティティとそれに関連するStudentエンティティに明示的な読み込みを追加します。(明示的なロードは実際には不要ですが、ここでは明示的なロードの実行方法のみを示しています)
インストラクターのインデックス付きビューのViewModelを作成する
講師のページには3つの異なるテーブルが表示されるため、3つの属性を持つビューモデルを作成します。各属性には、テーブルに必要なデータが保持されます。
ViewModelsフォルダーでInstructorIndexData.csを作成し、元のファイルを次のコードに置き換えます。
using System.Collections.Generic;
using ContosoUniversity.Models;
namespace ContosoUniversity.ViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
インストラクターのコントローラーとビューを作成する
前のCourseControllerコントローラと同様に、以下に示すようにInstructorControllerコントローラを作成します。
Controller \ InstructorController.csを開き、ViewModelsの名前空間参照を追加します。
using ContosoUniversity.ViewModels;
スキャフォールディングによって作成されたIndexメソッドコードでは、OfficeAssignmentナビゲーションプロパティのみがプリロードされています。
public ActionResult Index()
{
var instructors = db.Instructors.Include(i => i.OfficeAssignment);
return View(instructors.ToList());
}
次のコードを使用して、Indexメソッドを置き換え、他の関連データをロードします。
public ActionResult Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName);
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(
i => i.ID == id.Value).Single().Courses;
}
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
このメソッドは、オプションのルーティングパラメーター(id)とクエリ文字列パラメーター(courseID)を受け取り、選択した講師とコースのID値を提供し、必要なすべてのデータをビューに渡します。ページ上の選択ハイパーリンクは、これらのパラメーターを提供します。
このコードは、最初にビューモデルのインスタンスを作成し、講師のリストをモデルに配置します。コードは、OfficeAssignmentおよびCoursesナビゲーションプロパティでのプリロードの使用を指定します。
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName);
2番目のIncludeメソッドは、コースをロードし、各コースの部門ナビゲーションプロパティをプリロードします。
.Include(i => i.Courses.Select(c => c.Department))
先に述べたように、パフォーマンスを改善する場合を除いて、事前ロードは必要ありません。ビューは常にOfficeAssgnmentエンティティを必要とするため、同じクエリでビューを処理する方が効果的です。講師が選択されたときにコースエンティティを読み込む必要があります。ページがコースを選択せずに表示するよりも頻繁に表示される場合にのみ、事前読み込みは遅延読み込みよりも優れています。
講師IDを選択すると、選択した講師がビューモデル一覧から取得されます。ビューモデルのコースプロパティは、インストラクターのコースナビゲーションプロパティを通じて関連するコースエンティティを読み込みます。
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}
Whereメソッドはコレクションを返しますが、ここでは講師エンティティのみを返します。Singleメソッドは、コレクションを講師エンティティに変換し、エンティティのCoursesプロパティにアクセスできるようにします。
コレクションに含まれる要素が1つだけであることがわかっている場合は、コレクションに対してSingleメソッドを使用できます。空のコレクションまたは複数の要素を持つコレクションでSingleメソッドを呼び出すと、例外が発生します。別のオプションは、SingleOrDefaultを使用することです。コレクションが空の場合、デフォルト値を返します。ただし、この例でSingleOrDefaultを使用すると、例外が発生し(コースプロパティにアクセスしようとしますが、プロパティはnull参照です)、例外メッセージがこれを示します。Singleメソッドを呼び出すときは、WhereメソッドとSingleメソッドを別々に呼び出す代わりに、Where条件を渡すこともできます。
.Single(i => i.ID == id.Value)
代わりに
.Where(I => i.ID == id.Value).Single()
次に、コースを選択すると、選択したコースがビューモデルのコースリストから取得されます。次に、コースの登録ナビゲーションプロパティから登録エンティティを読み取り、ビューモデルの登録プロパティに読み込みます。
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
講師のインデックスビューを変更する
Views \ Instructor \ Index.cshtmlで、オリジナルを次のコードに置き換えます。
@model ContosoUniversity.ViewModels.InstructorIndexData
@{
ViewBag.Title = "Instructors";
}
<h2>Instructors</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th></th>
</tr>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == ViewBag.InstructorID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@Html.ActionLink("Select", "Index", new { id = item.ID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
@Html.ActionLink("Details", "Details", new { id = item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.ID })
</td>
</tr>
}
</table>
コードに次の変更を加えました。
- ビューのモデルクラスをInstructorIndexDataに変更します。
- タイトルを変更しました
- item.OfficeAssignentが空でない場合に場所を表示するオフィス列を追加(これは1対1またはゼロの関係であるため)
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
- コードを通じて、選択した教師のtr要素にclass = "success"を動的に追加します。ここで、選択した行の背景色は、Bootstrapスタイルシートを使用して設定されます。
string selectedRow = "";
if (item.InstructorID == ViewBag.InstructorID)
{
selectedRow = "success";
}
<tr class="@selectedRow" valign="top">
- 新しいActionLinkを追加して、選択した講師IDをIndexメソッドに送信します。
アプリケーションを実行し、「講師」タブを選択します。OfficeAssignmentエンティティが空でない場合は、講師の情報とLocationプロパティがページに表示されます。関連するオフィスがない場合、何も表示されません。
Views \ Instructor \ Index.cshtmlファイルで、テーブル要素の後に次のコードを追加して、選択した講師のコースリストを表示します
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Courses)
{
string selectedRow = "";
if (item.CourseID == ViewBag.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
このコードは、ビューモジュールでコース属性を読み取って表示するために使用されます。また、選択したコースのIDをIndexメソッドに送信するための選択ハイパーリンクも提供します。
ページを実行して講師を選択すると、その講師に割り当てられているコースを示す表が表示されます。
追加したコードの後に次のコードを追加して、選択したコースに登録されている学生のリストを表示します。
@if (Model.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
このコードは、ビューモデルのEnrollmentsプロパティを読み取り、コースに登録する学生を表示するために使用されます。
ページを実行して講師を選択し、コースを選択して登録済みの学生とその成績を表示します。
明示的な読み込みを追加
InstructorController.csを開き、Indexメソッドが選択したコースの登録済みリストを取得する方法を確認します。
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
講師のリストを取得するときに、コースのナビゲーション属性と各コースの部門属性にプリロードを指定しました。次に、コースコレクションをビューモデルに配置します。これで、コレクションのエンティティにナビゲーションプロパティを登録することにより、コースコレクションにアクセスできます。Navigation.Enrollmentsナビゲーションプロパティのプリロードを指定しなかったため、このプロパティのデータは遅延読み込みを使用し、ページがレンダリングされるときにのみ読み込まれます。
他のコードを変更せずに遅延読み込みを無効にすると、実際に登録がいくつ行われても、Enrollmentsプロパティは空になります。この場合、登録属性をロードする場合は、事前ロードまたは明示的なロードを指定する必要があります。プリロードの使用方法についてはすでに説明しました。明示的な読み込みを示すために、元の学生の部分を次のコードで置き換えます。Enrollmentsプロパティで明示的な読み込みを使用します。
public ActionResult Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName);
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(
i => i.ID == id.Value).Single().Courses;
}
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
// Lazy loading
//viewModel.Enrollments = viewModel.Courses.Where(
// x => x.CourseID == courseID).Single().Enrollments;
// Explicit loading
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
db.Entry(enrollment).Reference(x => x.Student).Load();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
Courseエンティティを選択した後、新しいコードは明示的にロードされたコースのEnrollmentsナビゲーションプロパティを使用します。
db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
次に、各Enrollmentエンティティに関連付けられたStudentエンティティを明示的に読み込みます。
db.Entry(enrollment).Reference(x => x.Student).Load();
Collectionメソッドを使用してコレクションをロードしますが、エンティティが1つだけの属性の場合は、Referenceメソッドを使用してロードすることに注意してください。
次にページを再実行して、すべてが正常に機能していることを確認しますが、実際にはデータの取得方法が変更されています。
まとめ
これで、遅延、事前ロード、および明示的なロードメソッドを使用して、関連データをナビゲーションプロパティにロードしようとしました。次のチュートリアルでは、関連データを更新する方法を学習します。
著者情報
Tom Dykstra -Tom Dykstraは、Microsoft Web Platform and Toolsチームのシニアプログラマー兼ライターです。