チュートリアル: 関連データを読み取る - ASP.NET MVC と EF Core
前のチュートリアルでは、School データ モデルを作成しました。 このチュートリアルでは、関連データ (Entity Framework がナビゲーション プロパティに読み込むデータ) の読み取りと表示を行います。
以下の図は、使用するページを示しています。
このチュートリアルでは、次の作業を行いました。
- 関連データを読み込む方法を学習する
- Courses ページを作成する
- Instructors ページを作成する
- 明示的読み込みについて学習する
必須コンポーネント
関連データを読み込む方法を学習する
オブジェクト リレーショナル マッピング (ORM) ソフトウェア (Entity Framework など) では、次のように関連データをエンティティのナビゲーション プロパティに読み込むことができる方法がいくつかあります。
一括読み込み: エンティティが読み取られるときに、関連データがエンティティと共に取得されます。 これは通常、必要なデータをすべて取得する 1 つの結合クエリになります。
Include
メソッドとThenInclude
メソッドを使用して、Entity Framework Core で一括読み込みを指定します。分離したクエリのデータの一部を取得することができ、EF によってナビゲーション プロパティが "修正されます"。 つまり、EF で、前に取得したエンティティのナビゲーション プロパティに属する、個別に取得したエンティティを自動的に追加します。 関連データを取得するクエリの場合、リストやオブジェクトを返すメソッド (
ToList
、Single
など) の代わりに、Load
メソッドを使用できます。明示的読み込み: エンティティが最初に読み込まれるときに、関連データは取得されません。 必要な場合に、関連データを取得するコードを記述します。 分離したクエリによる一括読み込みのように、明示的読み込みでは、複数のクエリがデータベースに送信されます。 明示的読み込みを使用すると、コードで読み込まれるナビゲーション プロパティを指定する点が異なります。 Entity Framework Core 1.1 では、
Load
メソッドを使用して、明示的読み込みを実行できます。 次に例を示します。遅延読み込み: エンティティが最初に読み込まれるときに、関連データは取得されません。 ただし、ナビゲーション プロパティに初めてアクセスしようとすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティからデータを取得しようとするたびに、クエリがデータベースに送信されます。 Entity Framework Core 1.0 では、遅延読み込みはサポートされません。
パフォーマンスに関する考慮事項
取得したすべてのエンティティの関連データが必要な場合は、通常、データベースに送信された 1 つのクエリの方が、取得した各エンティティに対する分離したクエリよりも効率的なため、一括読み込みを使用すると、より最適なパフォーマンスが得られます。 たとえば、各部門に関連コースが 10 個あるとします。 すべての関連データの一括読み込みは、1 つの (結合) クエリと 1 回のデータベースとのラウンド トリップだけになります。 部門ごとのコースへの分離したクエリでは、データベースとのラウンド トリップが 11 回行われることになります。 データベースとの余分なラウンド トリップは、特に待ち時間が長いときのパフォーマンスに悪影響をもたらします。
その一方で、一部のシナリオでは、分離したクエリがより効率的です。 1 つのクエリ内のすべての関連データの一括読み込みでは、SQL Server で効率的に処理できない、複雑な結合が生成される場合があります。 または、処理しているエンティティのセットのサブセットのためだけにエンティティのナビゲーション プロパティにアクセスする必要がある場合、事前のすべてを取得する一括読み込みでは、必要以上にデータを取得するため、分離したクエリの方が適切に実行される可能性があります。 パフォーマンスが重要な場合、最適な選択を行うために、両方の方法でパフォーマンスをテストすることをお勧めします。
Courses ページを作成する
Course
エンティティには、コースが割り当てられている部門の Department
エンティティを含む、ナビゲーション プロパティが含まれます。 コースのリストに割り当てられた部門の名前を表示するには、Course.Department
ナビゲーション プロパティにある Department
エンティティから Name
プロパティを取得する必要があります。
次の図に示すように、StudentsController
に対して前に実行した Entity Framework のスキャフォールディング機能を使用して、ビューと共に MVC コントローラーに同じオプションを使用して、Course
エンティティ型の CoursesController
という名前のコントローラーを作成します。
CoursesController.cs
を開き、Index
メソッドを調べます。 自動スキャフォールディングでは、Include
メソッドを使って、Department
ナビゲーション プロパティに一括読み込みを指定しています。
Index
メソッドを、Course エンティティ (schoolContext
の代わりに courses
) を返す IQueryable
により適切な名前を使用する次のコードに置き換えます。
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}
Views/Courses/Index.cshtml
を開き、テンプレート コードを次のコードに置き換えます。 変更が強調表示されています。
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@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>
<a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
スキャフォールディング コードに、次の変更を行いました。
見出しが Index から Courses に変更されました。
CourseID
プロパティ値を示す Number 列が追加されました。 既定では、主キーは、通常、エンド ユーザーにとって意味がないため、スキャフォールディングされません。 ただし、このケースでは、主キーは意味があり、表示する必要があります。部門名が表示されるように、Department 列を変更しました。 コードは、
Department
ナビゲーション プロパティに読み込まれるDepartment
エンティティのName
プロパティを表示します。@Html.DisplayFor(modelItem => item.Department.Name)
アプリを実行し、 [Courses] タブを選択して部門名のリストを表示します。
Instructors ページを作成する
このセクションでは、Instructors ページを表示するために、Instructor
エンティティのコントローラーとビューを作成します。
このページは、次の方法で関連データを読み取って表示します。
インストラクターのリストには、
OfficeAssignment
エンティティからの関連データが表示されます。Instructor
エンティティとOfficeAssignment
エンティティは、一対ゼロまたは一対一のリレーションシップです。OfficeAssignment
エンティティに一括読み込みを使用します。 前述のように、通常、一括読み込みは、主テーブルで取得したすべての行の関連データが必要なときにより効率的です。 このケースでは、割り当てられたすべてのインストラクターのオフィスの割り当てを表示する必要があります。ユーザーがインストラクターを選択すると、関連する
Course
エンティティが表示されます。Instructor
エンティティとCourse
エンティティは多対多リレーションシップです。Course
エンティティとその関連Department
エンティティの一括読み込みを使用します。 このケースでは、選択したインストラクターのコースのみが必要なため、分離したクエリの方が効率的な可能性があります。 ただし、この例では、ナビゲーション プロパティにあるエンティティ内のナビゲーション プロパティに一括読み込みを使用する方法を示します。ユーザーがコースを選択すると、
Enrollments
エンティティ セットからの関連データが表示されます。Course
エンティティとEnrollment
エンティティは一対多リレーションシップです。Enrollment
エンティティとそれに関連するStudent
エンティティに分離したクエリを使用します。
Instructor インデックス ビューのビュー モデルを作成する
Instructors ページには、3 つの異なるテーブルからデータが表示されます。 そのため、テーブルの 1 つにデータを保持するごとに、3 つのプロパティを含むビュー モデルを作成します。
SchoolViewModels フォルダー内に InstructorIndexData.cs
を作成し、既存のコードを次のコードで置き換えます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
Instructor コントローラーとビューを作成する
次の図に示すように、EF の読み取り/書き込みアクションで、Instructors コントローラーを作成します。
InstructorsController.cs
を開いて、ViewModels 名前空間に対する using ステートメントを追加します。
using ContosoUniversity.Models.SchoolViewModels;
Index メソッドを次のコードに置き換えて、関連データの一括読み込みを行い、ビュー モデルに配置します。
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
メソッドでは、選択したインストラクターと選択したコースの ID 値を指定する、オプションのルート データ (id
) とクエリ文字列パラメーター (courseID
) を受け入れます。 パラメーターは、ページの Select ハイパーリンクによって指定されます。
このコードは、ビュー モデルのインスタンスを作成し、インストラクターのリストに配置することから始めます。 コードでは、Instructor.OfficeAssignment
と Instructor.CourseAssignments
ナビゲーション プロパティに一括読み込みを指定します。 CourseAssignments
プロパティ内で Course
プロパティが読み込まれ、そのプロパティ内で Enrollments
と Department
プロパティが読み込まれ、各 Enrollment
エンティティ内で Student
プロパティが読み込まれます。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
ビューには常に OfficeAssignment
エンティティが必要なため、同じクエリでフェッチする方が効率的です。 インストラクターが Web ページで選択されたときに、Course エンティティが必要なため、1 つのクエリが複数のクエリよりも適しているのは、ページに選択したコースを含めないよりも、含めて表示することの方が多い場合のみです。
Course
から 2 つのプロパティが必要なため、コードでは CourseAssignments
と Course
を繰り返します。 ThenInclude
呼び出しの最初の文字列では、CourseAssignment.Course
、Course.Enrollments
、および Enrollment.Student
を取得します。
関連データの複数のレベルを含める方法の詳細については、こちらを参照してください。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
コードのその時点で、もう 1 つの ThenInclude
は、必要としない Student
のナビゲーション プロパティ用になります。 ただし、Include
を呼び出すと、Instructor
プロパティを使ってやり直されるため、もう一度チェーンを順に移動する必要があります。今回は Course.Enrollments
の代わりに Course.Department
を指定しています。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
次のコードは、インストラクターが選択されたときに実行されます。 選択されたインストラクターがビュー モデルのインストラクターのリストから取得されます。 次に、ビュー モデルの Courses
プロパティが Course
エンティティと共にそのインストラクターの CourseAssignments
ナビゲーション プロパティから読み込まれます。
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
Where
メソッドはコレクションを返しますが、このケースでは、そのメソッドに渡された条件は、返されている Instructor エンティティの 1 つのみになります。 Single
メソッドを実行すると、コレクションが、エンティティの CourseAssignments
プロパティへのアクセス権を付与する 1 つの Instructor
エンティティに変換されます。 CourseAssignments
プロパティには、関連する Course
エンティティのみを必要とする、CourseAssignment
エンティティが含まれます。
コレクションに項目が 1 つのみであることがわかっている場合、コレクションで Single
メソッドを使用します。 コレクションが空になる場合、または複数の項目がある場合、Single
メソッドから例外がスローされます。 代わりに、コレクションが空の場合に既定値 (この場合は null) を返す SingleOrDefault
を使用します。 ただし、(null 参照で Courses
プロパティを見つけようとして) 引き続き例外となる場合は、例外メッセージでは問題の原因があまり明確に示されません。 Single
メソッドを呼び出す場合、個別に Where
メソッドを呼び出す代わりに、Where 条件で渡すこともできます。
.Single(i => i.ID == id.Value)
これは次のコードの代わりに使用します。
.Where(i => i.ID == id.Value).Single()
次に、コースが選択された場合、選択したコースはビュー モデルのコースのリストから取得されます。 次に、ビュー モデルの Enrollments
プロパティが Enrollment エンティティと共にそのコースの Enrollments
ナビゲーション プロパティから読み込まれます。
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
追跡と追跡なし
追跡なしのクエリは、読み取り専用のシナリオで結果が使用される場合に役立ちます。 変更追跡情報を設定する必要がないため、通常は実行が速くなります。 データベースから取得したエンティティを更新する必要がない場合は、追跡クエリよりも追跡なしのクエリの方がパフォーマンスが向上する可能性があります。
場合によっては、追跡クエリは追跡なしのクエリよりも効率的です。 詳細については、「追跡と追跡なしのクエリ」を参照してください。
Instructor インデックス ビューを変更する
Views/Instructors/Index.cshtml
で、テンプレート コードを次のコードに置き換えます。 変更が強調表示されています。
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["InstructorID"])
{
selectedRow = "table-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>
@foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @course.Course.Title <br />
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["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>
@foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @course.Course.Title <br />
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
既存のコードに次の変更を行いました。
モデル クラスが
InstructorIndexData
に変更されました。Index のページ タイトルが Instructors に変更されました。
item.OfficeAssignment
が null ではない場合にのみitem.OfficeAssignment.Location
を表示する Office 列を追加しました。 (これは、一対ゼロまたは一対一のリレーションシップであるため、関連する OfficeAssignment エンティティがない場合があります。)@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
インストラクターごとに担当したコースを表示する Courses 列を追加しました。 詳細については、Razor 構文記事の「明示的な行の遷移」セクションを参照してください。
選択したインストラクターの
tr
要素にブートストラップ CSS クラスを条件付きで追加するコードを追加しました。 このクラスにより、選択した行の背景色が設定されます。Index
メソッドに送信される選択されたインストラクターの ID を発生させる、各行の他のリンクの直前に Select というラベルの新しいハイパーリンクを追加しました。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
アプリを実行し、 [Instructors] タブを選択します。関連する OfficeAssignment エンティティがない場合は、ページに関連する OfficeAssignment エンティティの Location プロパティと空の表のセルが表示されます。
Views/Instructors/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 == (int?)ViewData["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>
}
このコードでは、ビュー モデルの Courses
プロパティを読み取り、コースのリストを表示します。 また、選択したコースの ID を Index
アクション メソッドに送信する、Select ハイパーリンクも指定します。
ページを更新し、インストラクターを選択します。 選択したインストラクターに割り当てられたコースを表示するグリッドを表示し、各コースに割り当てられた部門の名前を表示します。
追加したコード ブロックの後に、次のコードを追加します。 このコードは、コースを選択したときに、コースに登録されている受講者のリストを表示します。
@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
プロパティを読み取ります。
もう一度ページを更新し、インストラクターを選択します。 次に、コースを選択して、登録済みの受講者とその成績のリストを表示します。
明示的読み込みについて
InstructorsController.cs
でインストラクターの一覧を取得したときに、CourseAssignments
ナビゲーション プロパティに一括読み込みを指定しました。
ユーザーは選択したインストラクターとコースの登録内容をほとんど表示する必要がないとします。 その場合は、要求された場合にのみ、登録データを読み取る必要がある可能性があります。 明示的読み込みを行う方法の例を表示するには、Index
メソッドを次のコードに置き換えます。このコードでは、Enrollments
の一括読み込みを削除して、そのプロパティを明示的に読み込みます。 コードの変更が強調表示されています。
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
新しいコードでは、インストラクター エンティティを取得するコードから登録データを呼び出す ThenInclude
メソッドを削除します。 AsNoTracking
も削除されます。 インストラクターとコースが選択された場合、強調表示されたコードによって、選択されたコードの Enrollment
エンティティ、および各 Enrollment
の Student
エンティティが取得されます。
アプリを実行して、Instructors/Index ページに移動すると、データを取得する方法を変更しているにもかかわらず、ページ上で表示される内容に変わりがないことがわかります。
コードを取得する
次の手順
このチュートリアルでは、次の作業を行いました。
- 関連データを読み込む方法を学習した
- Courses ページを作成した
- Instructors ページを作成した
- 明示的読み込みについて学習した
関連データを更新する方法について学習するには、次のチュートリアルに進んでください。
ASP.NET Core