パート 6、ASP.NET Core の Razor ページと EF Core - 関連データの読み込み
作成者: Tom Dykstra、Jon P Smith、Rick Anderson
Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。 チュートリアル シリーズについては、最初のチュートリアルを参照してください。
解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。
このチュートリアルでは、関連データを読み取って表示する方法を示します。 関連データとは、EF Core がナビゲーション プロパティに読み込むデータのことです。
以下の図は、このチュートリアルの完成したページを示しています。
一括読み込み、明示的読み込み、遅延読み込み
EF Core がエンティティのナビゲーション プロパティに関連データを読み込むには、次の複数の方法があります。
一括読み込み。 一括読み込みは、エンティティの 1 つの型に対するクエリが関連エンティティも読み込む場合です。 エンティティが読み取られるときに、その関連データが取得されます。 これは通常、必要なデータをすべて取得する 1 つの結合クエリになります。 EF Core は、一部の型の一括読み込みに対して複数のクエリを発行します。 複数のクエリを発行する方が、1 つの大規模なクエリよりも効率的である場合があります。 一括読み込みは、Include メソッドと ThenInclude メソッドを使用して指定されます。
一括読み込みでは、コレクション ナビゲーションが含まれるときに、複数のクエリが送信されます。
- メイン クエリに 1 つのクエリ
- 読み込みツリー内のコレクション "エッジ" ごとに 1 つのクエリ
Load
で分離したクエリ: データは分離したクエリで取得でき、EF Core がナビゲーション プロパティを "修正" します。 "修正" は、ナビゲーション プロパティが EF Core によって自動的に入力されることを意味します。Load
で分離したクエリは、一括読み込みよりも明示的読み込みに似ています。注:EF Core は、コンテキスト インスタンスに以前に読み込まれたその他のエンティティに対して、ナビゲーション プロパティを自動的に修正します。 ナビゲーション プロパティのデータが明示的に含まれない場合でも、関連エンティティの一部またはすべてが以前に読み込まれていれば、プロパティを設定することができます。
明示的読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 必要なときに関連するデータを取得するコードを記述する必要があります。 分離したクエリによる明示的読み込みにより、複数のクエリがデータベースに送信されます。 明示的読み込みでは、コードで読み込まれるナビゲーション プロパティを指定します。 明示的読み込みを行うには、
Load
メソッドを使用します。 次に例を示します。遅延読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 ナビゲーション プロパティに初めてアクセスすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティにアクセスされるたびに、クエリがデータベースに送信されます。 遅延読み込みでは、開発者が N + 1 クエリを使用する場合などにパフォーマンスが低下する可能性があります。 N + 1 クエリは親を読み込んで子を列挙します。
Course ページの作成
Course
エンティティには、関連する Department
エンティティを含むナビゲーション プロパティが含まれています。
コースに割り当てられている部署の名前を表示するには
- 関連する
Department
エンティティをCourse.Department
ナビゲーション プロパティに読み込みます。 Department
エンティティのName
プロパティから名前を取得します。
Course ページのスキャフォールディング
次の例外を除き、「Student ページのスキャフォールディング」の指示に従います。
- Pages/Courses フォルダーを作成します。
- モデル クラスに
Course
を使用します。 - 新しいコンテキスト クラスを作成するのではなく、既存のコンテキスト クラスを使用します。
Pages/Courses/Index.cshtml.cs
を開き、OnGetAsync
メソッドを調べます。 スキャフォールディング エンジンは、Department
ナビゲーション プロパティに一括読み込みを指定しました。Include
メソッドが一括読み込みを指定します。アプリを実行し、 [Courses] リンクを選択します。 Department 列に
DepartmentID
が表示されますが、これには役に立ちません。
部署名の表示
次のコードを使用して Pages/Courses/Index.cshtml.cs を更新します。
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public IList<Course> Courses { get; set; }
public async Task OnGetAsync()
{
Courses = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
}
}
上記のコードでは、Course
プロパティを Courses
に変更し、AsNoTracking
を追加します。
追跡なしのクエリは、読み取り専用のシナリオで結果が使用される場合に役立ちます。 変更追跡情報を設定する必要がないため、通常は実行が速くなります。 データベースから取得したエンティティを更新する必要がない場合は、追跡クエリよりも追跡なしのクエリの方がパフォーマンスが向上する可能性があります。
場合によっては、追跡クエリは追跡なしのクエリよりも効率的です。 詳細については、「追跡と追跡なしのクエリ」を参照してください。
前のコードでは、エンティティが現在のコンテキストで更新されないため、AsNoTracking
が呼び出されます。
Pages/Courses/Index.cshtml
を次のコードで更新します。
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}
<h1>Courses</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Courses[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Courses)
{
<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-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
スキャフォールディング コードに、次の変更が行われました。
Course
プロパティ名はCourses
に変更されました。CourseID
プロパティ値を示す Number 列が追加されました。 既定では、主キーは、通常、エンドユーザーにとって意味がないため、スキャフォールディングされません。 ただし、このケースでは、主キーは意味があります。部門名が表示されるように、Department 列を変更しました。 コードは、
Department
ナビゲーション プロパティに読み込まれるDepartment
エンティティのName
プロパティを表示します。@Html.DisplayFor(modelItem => item.Department.Name)
アプリを実行し、 [Courses] タブを選択して部門名のリストを表示します。
Select を使用した関連データの読み込み
OnGetAsync
メソッドでは、Include
メソッドを使用して関連データを読み込みます。 Select
メソッドは、必要な関連データだけを読み込む代替手段です。 Department.Name
のような単一の項目の場合、SQL INNER JOIN
が使用されます。 コレクションの場合は、別のデータベース アクセスが使用されますが、コレクションの Include
演算子でも同じです。
次のコードは、Select
メソッドを使用して関連データを読み込みます。
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
上記のコードではエンティティ型が返されないため、追跡は行われません。 EF の追跡の詳細については、「追跡と追跡なしのクエリ」を参照してください。
CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
完全な Razor のページについては、IndexSelectModel を参照してください。
Instructor ページの作成
このセクションでは、Instructor ページをスキャフォールディングし、関連する Courses と Enrollments を Instructors Index ページに追加します。
このページは、次の方法で関連データを読み取って表示します。
- インストラクターのリストには
OfficeAssignment
エンティティからの関連データが表示されます (上の図の Office)。Instructor
エンティティとOfficeAssignment
エンティティは、一対ゼロまたは一対一のリレーションシップです。OfficeAssignment
エンティティには一括読み込みが使用されています。 一括読み込みは一般的に、関連データを表示する必要がある場合により効率的です。 この場合、インストラクターへのオフィスの割り当てが表示されます。 - ユーザーがインストラクターを選択すると、関連する
Course
エンティティが表示されます。Instructor
エンティティとCourse
エンティティは多対多リレーションシップです。Course
エンティティとその関連Department
エンティティには一括読み込みが使用されます。 このケースでは、選択したインストラクターのコースのみが必要なため、分離したクエリの方が効率的な場合があります。 この例では、ナビゲーション プロパティ内のエンティティのナビゲーション プロパティに一括読み込みを使用する方法を示します。 - ユーザーがコースを選択すると、
Enrollments
エンティティからの関連データが表示されます。 上の図では、受講者名とグレードが表示されています。Course
エンティティとEnrollment
エンティティは一対多リレーションシップです。
ビュー モデルを作成する
Instructors ページには、3 つの異なるテーブルからのデータが表示されます。 3 つのテーブルを表す 3 つのエンティティを含むビュー モデルが必要です。
次のコードを使用して Models/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 ページのスキャフォールディング
次の例外を除き、「Student ページのスキャフォールディング」の指示に従います。
- Pages/Instructors フォルダーを作成します。
- モデル クラスに
Instructor
を使用します。 - 新しいコンテキスト クラスを作成するのではなく、既存のコンテキスト クラスを使用します。
アプリを実行し、Instructors ページに移動します。
次のコードを使用して Pages/Instructors/Index.cshtml.cs
を更新します。
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData InstructorData { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}
if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await _context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}
}
}
}
OnGetAsync
メソッドは、選択したインストラクターの ID の任意のルート データを受け取ります。
Pages/Instructors/Index.cshtml.cs
ファイルでクエリを調べます。
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.ThenInclude(c => c.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
このコードでは、次のナビゲーション プロパティに一括読み込みを指定します。
Instructor.OfficeAssignment
Instructor.Courses
Course.Department
次のコードは、インストラクターが選択されたとき (つまり id != null
のとき) に実行されます。
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.Courses;
}
選択されたインストラクターがビュー モデルのインストラクターのリストから取得されます。 ビュー モデルの Courses
プロパティが、選択されたインストラクターの Courses
ナビゲーション プロパティの Course
エンティティを使用して読み込まれます。
Where
メソッドはコレクションを返します。 この場合、フィルターによってエンティティが 1 つ選択されます。そのため、コレクションを 1 つの Instructor
エンティティに変換するために Single
メソッドが呼び出されます。 Instructor
エンティティは Course
ナビゲーション プロパティへのアクセスを提供します。
コレクションに 1 つの項目しかない場合は、Single メソッドがコレクションで使用されます。 コレクションが空の場合、または複数の項目がある場合、Single
メソッドは例外をスローします。 あるいは、コレクションが空の場合に既定値を返す SingleOrDefault を使用します。 このクエリの場合、既定では null
が返されます。
次のコードは、コースが選択されたときにビュー モデルの Enrollments
プロパティを設定します。
if (courseID != null)
{
CourseID = courseID.Value;
IEnumerable<Enrollment> Enrollments = await _context.Enrollments
.Where(x => x.CourseID == CourseID)
.Include(i=>i.Student)
.ToListAsync();
InstructorData.Enrollments = Enrollments;
}
Instructors/Index ページを更新する
Pages/Instructors/Index.cshtml
を次のコードで更新します。
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-page="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.InstructorData.Instructors)
{
string selectedRow = "";
if (item.ID == Model.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.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
<td>
<a asp-page="./Index" asp-route-id="@item.ID">Select</a> |
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.InstructorData.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.InstructorData.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
@if (Model.InstructorData.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.InstructorData.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
上記のコードは、次の変更を加えます。
page
ディレクティブを@page "{id:int?}"
に更新します。"{id:int?}"
はルート テンプレートです。 ルート テンプレートは、URL 内の整数クエリ文字列をルート データに変更します。 たとえば、@page
ディレクティブのみのインストラクターで [Select] リンクをクリックすると、次のような URL を生成します。https://localhost:5001/Instructors?id=2
ページ ディレクティブが
@page "{id:int?}"
の場合、URL はhttps://localhost:5001/Instructors/2
のようになります。item.OfficeAssignment
が null ではない場合にのみ、item.OfficeAssignment.Location
を表示する Office 列を追加します。 これは、一対ゼロまたは一対一のリレーションシップであるため、関連する OfficeAssignment エンティティがない場合があります。@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
インストラクターごとに担当したコースを表示する Courses 列を追加します。 この razor 構文の詳細については、「明示的な行の遷移」をご覧ください。
選択したインストラクターとコースの
tr
要素にclass="table-success"
を動的に追加するコードを追加します。 これは、ブートストラップ クラスを使用して、選択した行の背景色を設定します。string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "table-success"; } <tr class="@selectedRow">
Select とラベル付けされる新しいハイパーリンクを追加します。 このリンクは、選択したインストラクターの ID を
Index
メソッドに送信し、背景色を設定します。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
選択した Instructor のコースのテーブルを追加します。
選択したコースの学生登録のテーブルを追加します。
アプリを実行し、 [Instructors] タブを選択します。関連する OfficeAssignment
エンティティから Location
(オフィス) がページに表示されます。 OfficeAssignment
が null の場合、空のテーブル セルが表示されます。
インストラクターの Select リンクをクリックします。 行スタイルの変更とそのインストラクターに割り当てられたコースが表示されます。
コースを選択して、登録済みの受講者とその成績のリストを表示します。
次の手順
次のチュートリアルでは、関連データの更新方法を示します。
このチュートリアルでは、関連データを読み取って表示する方法を示します。 関連データとは、EF Core がナビゲーション プロパティに読み込むデータのことです。
以下の図は、このチュートリアルの完成したページを示しています。
一括読み込み、明示的読み込み、遅延読み込み
EF Core がエンティティのナビゲーション プロパティに関連データを読み込むには、次の複数の方法があります。
一括読み込み。 一括読み込みは、エンティティの 1 つの型に対するクエリが関連エンティティも読み込む場合です。 エンティティが読み取られるときに、その関連データが取得されます。 これは通常、必要なデータをすべて取得する 1 つの結合クエリになります。 EF Core は、一部の型の一括読み込みに対して複数のクエリを発行します。 複数のクエリを発行することは、1 つの巨大なクエリよりも効率的である可能性があります。 一括読み込みは、
Include
メソッドとThenInclude
メソッドを使用して指定されます。一括読み込みでは、コレクション ナビゲーションが含まれるときに、複数のクエリが送信されます。
- メイン クエリに 1 つのクエリ
- 読み込みツリー内のコレクション "エッジ" ごとに 1 つのクエリ
Load
で分離したクエリ: データは分離したクエリで取得でき、EF Core がナビゲーション プロパティを "修正" します。 "修正" は、ナビゲーション プロパティが EF Core によって自動的に入力されることを意味します。Load
で分離したクエリは、一括読み込みよりも明示的読み込みに似ています。注:EF Core は、コンテキスト インスタンスに以前に読み込まれたその他のエンティティに対して、ナビゲーション プロパティを自動的に修正します。 ナビゲーション プロパティのデータが明示的に含まれない場合でも、関連エンティティの一部またはすべてが以前に読み込まれていれば、プロパティを設定することができます。
明示的読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 必要なときに関連するデータを取得するコードを記述する必要があります。 分離したクエリによる明示的読み込みにより、複数のクエリがデータベースに送信されます。 明示的読み込みでは、コードで読み込まれるナビゲーション プロパティを指定します。 明示的読み込みを行うには、
Load
メソッドを使用します。 次に例を示します。遅延読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 ナビゲーション プロパティに初めてアクセスすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティにアクセスされるたびに、クエリがデータベースに送信されます。 開発者が N + 1 パターンを使用して、親を読み込んで子を列挙する場合など、遅延読み込みでパフォーマンスが低下することがあります。
Course ページの作成
Course
エンティティには、関連する Department
エンティティを含むナビゲーション プロパティが含まれています。
コースに割り当てられている部署の名前を表示するには
- 関連する
Department
エンティティをCourse.Department
ナビゲーション プロパティに読み込みます。 Department
エンティティのName
プロパティから名前を取得します。
Course ページのスキャフォールディング
次の例外を除き、「Student ページのスキャフォールディング」の指示に従います。
- Pages/Courses フォルダーを作成します。
- モデル クラスに
Course
を使用します。 - 新しいコンテキスト クラスを作成するのではなく、既存のコンテキスト クラスを使用します。
Pages/Courses/Index.cshtml.cs
を開き、OnGetAsync
メソッドを調べます。 スキャフォールディング エンジンは、Department
ナビゲーション プロパティに一括読み込みを指定しました。Include
メソッドが一括読み込みを指定します。アプリを実行し、 [Courses] リンクを選択します。 Department 列に
DepartmentID
が表示されますが、これには役に立ちません。
部署名の表示
次のコードを使用して Pages/Courses/Index.cshtml.cs を更新します。
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Courses
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public IList<Course> Courses { get; set; }
public async Task OnGetAsync()
{
Courses = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
}
}
上記のコードでは、Course
プロパティを Courses
に変更し、AsNoTracking
を追加します。 AsNoTracking
は、返されるエンティティが追跡されないため、パフォーマンスが向上します。 エンティティは現在のコンテキストでは更新されないため、追跡する必要はありません。
Pages/Courses/Index.cshtml
を次のコードで更新します。
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}
<h1>Courses</h1>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Courses[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Courses[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Courses)
{
<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-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
スキャフォールディング コードに、次の変更が行われました。
Course
プロパティ名はCourses
に変更されました。CourseID
プロパティ値を示す Number 列が追加されました。 既定では、主キーは、通常、エンドユーザーにとって意味がないため、スキャフォールディングされません。 ただし、このケースでは、主キーは意味があります。部門名が表示されるように、Department 列を変更しました。 コードは、
Department
ナビゲーション プロパティに読み込まれるDepartment
エンティティのName
プロパティを表示します。@Html.DisplayFor(modelItem => item.Department.Name)
アプリを実行し、 [Courses] タブを選択して部門名のリストを表示します。
Select を使用した関連データの読み込み
OnGetAsync
メソッドでは、Include
メソッドを使用して関連データを読み込みます。 Select
メソッドは、必要な関連データだけを読み込む代替手段です。 Department.Name
のような単一の項目の場合、SQL INNER JOIN が使用されます。 コレクションの場合は、別のデータベース アクセスが使用されますが、コレクションの Include
演算子でも同じです。
次のコードは、Select
メソッドを使用して関連データを読み込みます。
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
上記のコードではエンティティ型が返されないため、追跡は行われません。 EF の追跡の詳細については、「追跡と追跡なしのクエリ」を参照してください。
CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
完全な例については、IndexSelect.cshtml と IndexSelect.cshtml.cs を参照してください。
Instructor ページの作成
このセクションでは、Instructor ページをスキャフォールディングし、関連する Courses と Enrollments を Instructors Index ページに追加します。
このページは、次の方法で関連データを読み取って表示します。
- インストラクターのリストには
OfficeAssignment
エンティティからの関連データが表示されます (上の図の Office)。Instructor
エンティティとOfficeAssignment
エンティティは、一対ゼロまたは一対一のリレーションシップです。OfficeAssignment
エンティティには一括読み込みが使用されています。 一括読み込みは一般的に、関連データを表示する必要がある場合により効率的です。 この場合、インストラクターへのオフィスの割り当てが表示されます。 - ユーザーがインストラクターを選択すると、関連する
Course
エンティティが表示されます。Instructor
エンティティとCourse
エンティティは多対多リレーションシップです。Course
エンティティとその関連Department
エンティティには一括読み込みが使用されます。 このケースでは、選択したインストラクターのコースのみが必要なため、分離したクエリの方が効率的な場合があります。 この例では、ナビゲーション プロパティ内のエンティティのナビゲーション プロパティに一括読み込みを使用する方法を示します。 - ユーザーがコースを選択すると、
Enrollments
エンティティからの関連データが表示されます。 上の図では、受講者名とグレードが表示されています。Course
エンティティとEnrollment
エンティティは一対多リレーションシップです。
ビュー モデルを作成する
Instructors ページには、3 つの異なるテーブルからのデータが表示されます。 3 つのテーブルを表す 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 ページのスキャフォールディング
次の例外を除き、「Student ページのスキャフォールディング」の指示に従います。
- Pages/Instructors フォルダーを作成します。
- モデル クラスに
Instructor
を使用します。 - 新しいコンテキスト クラスを作成するのではなく、既存のコンテキスト クラスを使用します。
更新する前にスキャフォールディング ページがどのように表示されるかを確認するには、アプリを実行し、Instructors ページに移動します。
次のコードを使用して Pages/Instructors/Index.cshtml.cs
を更新します。
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData InstructorData { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = InstructorData.Courses
.Where(x => x.CourseID == courseID).Single();
InstructorData.Enrollments = selectedCourse.Enrollments;
}
}
}
}
OnGetAsync
メソッドは、選択したインストラクターの ID の任意のルート データを受け取ります。
Pages/Instructors/Index.cshtml.cs
ファイルでクエリを調べます。
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
このコードでは、次のナビゲーション プロパティに一括読み込みを指定します。
Instructor.OfficeAssignment
Instructor.CourseAssignments
CourseAssignments.Course
Course.Department
Course.Enrollments
Enrollment.Student
CourseAssignments
と Course
に対する Include
メソッドと ThenInclude
メソッドの繰り返しに注意してください。 この繰り返しは、Course
エンティティの 2 つのナビゲーション プロパティに一括読み込みを指定するために必要です。
次のコードは、インストラクターが選択されたとき (id != null
) に実行されます。
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
選択されたインストラクターがビュー モデルのインストラクターのリストから取得されます。 ビュー モデルの Courses
プロパティが Course
エンティティと共にそのインストラクターの CourseAssignments
ナビゲーション プロパティから読み込まれます。
Where
メソッドはコレクションを返します。 ただし、この場合、フィルターによってエンティティが 1 つ選択されます。そのため、コレクションを 1 つの Instructor
エンティティに変換する目的で Single
メソッドが呼び出されます。 Instructor
エンティティは CourseAssignments
プロパティへのアクセスを提供します。 CourseAssignments
は関連する Course
エンティティへのアクセスを提供します。
コレクションに 1 つの項目しかない場合は、Single
メソッドがコレクションで使用されます。 コレクションが空の場合、または複数の項目がある場合、Single
メソッドは例外をスローします。 代わりに、コレクションが空の場合に既定値 (この場合は null) を返す SingleOrDefault
を使用します。
次のコードは、コースが選択されたときにビュー モデルの Enrollments
プロパティを設定します。
if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = InstructorData.Courses
.Where(x => x.CourseID == courseID).Single();
InstructorData.Enrollments = selectedCourse.Enrollments;
}
Instructors/Index ページを更新する
Pages/Instructors/Index.cshtml
を次のコードで更新します。
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-page="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.InstructorData.Instructors)
{
string selectedRow = "";
if (item.ID == Model.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-page="./Index" asp-route-id="@item.ID">Select</a> |
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.InstructorData.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.InstructorData.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "table-success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
@if (Model.InstructorData.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.InstructorData.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
上記のコードは、次の変更を加えます。
page
ディレクティブを@page
から@page "{id:int?}"
に更新します。"{id:int?}"
はルート テンプレートです。 ルート テンプレートは、URL 内の整数クエリ文字列をルート データに変更します。 たとえば、@page
ディレクティブのみのインストラクターで [Select] リンクをクリックすると、次のような URL を生成します。https://localhost:5001/Instructors?id=2
ページ ディレクティブが
@page "{id:int?}"
の場合、URL は次のようになります。https://localhost:5001/Instructors/2
item.OfficeAssignment
が null ではない場合にのみ、item.OfficeAssignment.Location
を表示する Office 列を追加します。 これは、一対ゼロまたは一対一のリレーションシップであるため、関連する OfficeAssignment エンティティがない場合があります。@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
インストラクターごとに担当したコースを表示する Courses 列を追加します。 この razor 構文の詳細については、「明示的な行の遷移」をご覧ください。
選択したインストラクターとコースの
tr
要素にclass="table-success"
を動的に追加するコードを追加します。 これは、ブートストラップ クラスを使用して、選択した行の背景色を設定します。string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "table-success"; } <tr class="@selectedRow">
Select とラベル付けされる新しいハイパーリンクを追加します。 このリンクは、選択したインストラクターの ID を
Index
メソッドに送信し、背景色を設定します。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
選択した Instructor のコースのテーブルを追加します。
選択したコースの学生登録のテーブルを追加します。
アプリを実行し、 [Instructors] タブを選択します。関連する OfficeAssignment
エンティティから Location
(オフィス) がページに表示されます。 OfficeAssignment
が null の場合、空のテーブル セルが表示されます。
インストラクターの Select リンクをクリックします。 行スタイルの変更とそのインストラクターに割り当てられたコースが表示されます。
コースを選択して、登録済みの受講者とその成績のリストを表示します。
Single を使用する
Single
メソッドは、Where
メソッドを別に呼び出す代わりに、Where
条件で渡すことができます。
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors.Single(
i => i.ID == id.Value);
InstructorData.Courses = instructor.CourseAssignments.Select(
s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
InstructorData.Enrollments = InstructorData.Courses.Single(
x => x.CourseID == courseID).Enrollments;
}
}
Where 条件を使った Single
の使用は、個人の好みの問題です。 Where
メソッドを使用しても利点はありません。
明示的読み込み
現在のコードは、Enrollments
と Students
に一括読み込みを指定します。
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
ユーザーがコースの登録を表示することはほとんどないとします。 その場合、最適化は要求された場合にのみ登録データを読み込むことです。 このセクションでは、Enrollments
と Students
の明示的読み込みを使用するために OnGetAsync
が更新されます。
Pages/Instructors/Index.cshtml.cs
を次のコードで更新します。
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData InstructorData { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
//.Include(i => i.CourseAssignments)
// .ThenInclude(i => i.Course)
// .ThenInclude(i => i.Enrollments)
// .ThenInclude(i => i.Student)
//.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors
.Where(i => i.ID == id.Value).Single();
InstructorData.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = InstructorData.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();
}
InstructorData.Enrollments = selectedCourse.Enrollments;
}
}
}
}
上記のコードは、登録と学生データの ThenInclude メソッド呼び出しを破棄します。 コースが選択されると、明示的読み込みコードで以下が取得されます。
- 選択したコースの
Enrollment
エンティティ。 - 各
Enrollment
のStudent
エンティティ。
上記のコードでは、.AsNoTracking()
がコメント アウトされていることに注目してください。 追跡対象のエンティティに対して、ナビゲーション プロパティのみを明示的に読み込むことができます。
アプリをテストします。 ユーザーの観点からは、アプリの動作は以前のバージョンと同じです。
次の手順
次のチュートリアルでは、関連データの更新方法を示します。
このチュートリアルでは、関連データが読み取られ、表示されます。 関連データとは、EF Core がナビゲーション プロパティに読み込むデータのことです。
解決できない問題が発生した場合は、完成したアプリをダウンロードまたは表示してください。ダウンロードの方法はこちらをご覧ください。
以下の図は、このチュートリアルの完成したページを示しています。
関連データの一括読み込み、明示的読み込み、遅延読み込み
EF Core がエンティティのナビゲーション プロパティに関連データを読み込むには、次の複数の方法があります。
一括読み込み。 一括読み込みは、エンティティの 1 つの型に対するクエリが関連エンティティも読み込む場合です。 エンティティが読み取られるときに、その関連データが取得されます。 これは通常、必要なすべてのデータを取得する 1 つの結合クエリになります。 EF Core は、一部の型の一括読み込みに対して複数のクエリを発行します。 複数のクエリを発行することで、1 つのクエリしかなかった EF6 の一部のクエリよりも、効率を高めることができます。 一括読み込みは、
Include
メソッドとThenInclude
メソッドを使用して指定されます。一括読み込みでは、コレクション ナビゲーションが含まれるときに、複数のクエリが送信されます。
- メイン クエリに 1 つのクエリ
- 読み込みツリー内のコレクション "エッジ" ごとに 1 つのクエリ
Load
で分離したクエリ: データは分離したクエリで取得でき、EF Core がナビゲーション プロパティを "修正" します。 "修正" は、ナビゲーション プロパティが EF Core によって自動的に入力されることを意味します。Load
で分離したクエリは、一括読み込みよりも明示的読み込みに似ています。注: EF Core は、コンテキスト インスタンスに以前に読み込まれたその他のエンティティに対して、ナビゲーション プロパティを自動的に修正します。 ナビゲーション プロパティのデータが明示的に含まれない場合でも、関連エンティティの一部またはすべてが以前に読み込まれていれば、プロパティを設定することができます。
明示的読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 必要なときに関連するデータを取得するコードを記述する必要があります。 分離したクエリによる明示的読み込みにより、複数のクエリが DB に送信されます。 明示的読み込みでは、コードで読み込まれるナビゲーション プロパティを指定します。 明示的読み込みを行うには、
Load
メソッドを使用します。 次に例を示します。遅延読み込み。 遅延読み込みがバージョン 2.1 の EF Core に追加されました。 エンティティが最初に読み込まれるときに、関連データは取得されません。 ナビゲーション プロパティに初めてアクセスすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティにアクセスされるたびに、クエリが DB に送信されます。
Select
演算子は必要な関連データのみを読み込みます。
部門名を表示する Course ページを作成する
Course エンティティには、Department
エンティティを含むナビゲーション プロパティが含まれています。 Department
エンティティには、コースが割り当てられる部門が含まれています。
コースの一覧で割り当てられている部門の名前を表示するには:
Department
エンティティからName
プロパティを取得します。Department
エンティティはCourse.Department
ナビゲーション プロパティから取得されます。
Course モデルのスキャフォールディング
「Student モデルをスキャホールディングする」の手順に従い、モデル クラスの Course
を使用します。
上記のコマンドは、Course
モデルをスキャフォールディングします。 Visual Studio でプロジェクトを開きます。
Pages/Courses/Index.cshtml.cs
を開き、OnGetAsync
メソッドを調べます。 スキャフォールディング エンジンは、Department
ナビゲーション プロパティに一括読み込みを指定しました。 Include
メソッドが一括読み込みを指定します。
アプリを実行し、 [Courses] リンクを選択します。 Department 列に DepartmentID
が表示されますが、これには役に立ちません。
OnGetAsync
メソッドを次のコードで更新します。
public async Task OnGetAsync()
{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
上のコードは AsNoTracking
を追加します。 AsNoTracking
は、返されるエンティティが追跡されないため、パフォーマンスが向上します。 これらのエンティティは現在のコンテキストでは更新されないため、追跡されません。
Pages/Courses/Index.cshtml
を、強調表示されている次のマークアップで更新します。
@page
@model ContosoUniversity.Pages.Courses.IndexModel
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Course[0].CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Course[0].Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Course)
{
<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-page="./Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-page="./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] タブを選択して部門名のリストを表示します。
Select を使用した関連データの読み込み
OnGetAsync
メソッドは、Include
メソッドを使用して関連データを読み込みます。
public async Task OnGetAsync()
{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
Select
演算子は必要な関連データのみを読み込みます。 Department.Name
のような単一の項目の場合、SQL INNER JOIN が使用されます。 コレクションの場合は、別のデータベース アクセスが使用されますが、コレクションの Include
演算子でも同じです。
次のコードは、Select
メソッドを使用して関連データを読み込みます。
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
完全な例については、IndexSelect.cshtml と IndexSelect.cshtml.cs を参照してください。
コース登録を示す Instructors ページを作成する
このセクションでは、Instructors ページが作成されます。
このページは、次の方法で関連データを読み取って表示します。
- インストラクターのリストには
OfficeAssignment
エンティティからの関連データが表示されます (上の図の Office)。Instructor
エンティティとOfficeAssignment
エンティティは、一対ゼロまたは一対一のリレーションシップです。OfficeAssignment
エンティティには一括読み込みが使用されています。 一括読み込みは一般的に、関連データを表示する必要がある場合により効率的です。 この場合、インストラクターへのオフィスの割り当てが表示されます。 - ユーザーがインストラクターを選択 (上の図では Harui) すると、関連
Course
エンティティが表示されます。Instructor
エンティティとCourse
エンティティは多対多リレーションシップです。Course
エンティティとその関連Department
エンティティには一括読み込みが使用されます。 このケースでは、選択したインストラクターのコースのみが必要なため、分離したクエリの方が効率的な場合があります。 この例では、ナビゲーション プロパティ内のエンティティのナビゲーション プロパティに一括読み込みを使用する方法を示します。 - ユーザーがコースを選択すると (上の図では Chemistry (化学))、
Enrollments
エンティティからの関連データが表示されます。 上の図では、受講者名とグレードが表示されています。Course
エンティティとEnrollment
エンティティは一対多リレーションシップです。
Instructor インデックス ビューのビュー モデルを作成する
Instructors ページには、3 つの異なるテーブルからのデータが表示されます。 3 つのテーブルを表す 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 モデルのスキャフォールディング
「Student モデルをスキャホールディングする」の手順に従い、モデル クラスの Instructor
を使用します。
上記のコマンドは、Instructor
モデルをスキャフォールディングします。
アプリを実行し、Instructors ページに移動します。
Pages/Instructors/Index.cshtml.cs
を次のコードに置き換えます。
using ContosoUniversity.Models;
using ContosoUniversity.Models.SchoolViewModels; // Add VM
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Instructors
{
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData Instructor { get; set; }
public int InstructorID { get; set; }
public async Task OnGetAsync(int? id)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
}
}
}
}
OnGetAsync
メソッドは、選択したインストラクターの ID の任意のルート データを受け取ります。
Pages/Instructors/Index.cshtml.cs
ファイルでクエリを調べます。
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
クエリには次の 2 つが含まれています。
OfficeAssignment
:Instructors ビューに表示されます。CourseAssignments
:担当したコースを取り込みます。
Instructors/Index ページを更新する
Pages/Instructors/Index.cshtml
を次のマークアップで更新します。
@page "{id:int?}"
@model ContosoUniversity.Pages.Instructors.IndexModel
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-page="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.Instructor.Instructors)
{
string selectedRow = "";
if (item.ID == Model.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-page="./Index" asp-route-id="@item.ID">Select</a> |
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
上記のマークアップは、次の変更を加えます。
page
ディレクティブを@page
から@page "{id:int?}"
に更新します。"{id:int?}"
はルート テンプレートです。 ルート テンプレートは、URL 内の整数クエリ文字列をルート データに変更します。 たとえば、@page
ディレクティブのみのインストラクターで [Select] リンクをクリックすると、次のような URL を生成します。http://localhost:1234/Instructors?id=2
ページ ディレクティブが
@page "{id:int?}"
の場合、上記の URL は次のようになります。http://localhost:1234/Instructors/2
ページ タイトルは Instructors です。
item.OfficeAssignment
が null ではない場合にのみitem.OfficeAssignment.Location
を表示する Office 列を追加しました。 これは、一対ゼロまたは一対一のリレーションシップであるため、関連する OfficeAssignment エンティティがない場合があります。@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
インストラクターごとに担当したコースを表示する Courses 列を追加しました。 この razor 構文の詳細については、「明示的な行の遷移」をご覧ください。
選択したインストラクターの
tr
要素にclass="success"
を動的に追加するコードを追加しました。 これは、ブートストラップ クラスを使用して、選択した行の背景色を設定します。string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "success"; } <tr class="@selectedRow">
Select とラベル付けされるハイパーリンクを追加しました。 このリンクは、選択したインストラクターの ID を
Index
メソッドに送信し、背景色を設定します。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
アプリを実行し、 [Instructors] タブを選択します。関連する OfficeAssignment
エンティティから Location
(オフィス) がページに表示されます。 OfficeAssignment` が null の場合、空のテーブル セルが表示されます。
[Select] リンクをクリックします。 行のスタイルが変更されます。
選択したインストラクターが担当するコースを追加する
Pages/Instructors/Index.cshtml.cs
の OnGetAsync
メソッドを次のコードで更新します。
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}
public int CourseID { get; set; }
を追加します
public class IndexModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public IndexModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
public InstructorIndexData Instructor { get; set; }
public int InstructorID { get; set; }
public int CourseID { get; set; }
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
}
更新されたクエリを確認します。
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
上記のクエリは Department
エンティティを追加します。
次のコードは、インストラクターが選択されたとき (id != null
) に実行されます。 選択されたインストラクターがビュー モデルのインストラクターのリストから取得されます。 ビュー モデルの Courses
プロパティが Course
エンティティと共にそのインストラクターの CourseAssignments
ナビゲーション プロパティから読み込まれます。
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
Where
メソッドはコレクションを返します。 上記の Where
メソッドでは、1 つの Instructor
エンティティのみが返されます。 Single
メソッドはコレクションを 1 つの Instructor
エンティティに変換します。 Instructor
エンティティは CourseAssignments
プロパティへのアクセスを提供します。 CourseAssignments
は関連する Course
エンティティへのアクセスを提供します。
コレクションに 1 つの項目しかない場合は、Single
メソッドがコレクションで使用されます。 コレクションが空の場合、または複数の項目がある場合、Single
メソッドは例外をスローします。 代わりに、コレクションが空の場合に既定値を返す (この場合は null) SingleOrDefault
を使用します。 空のコレクションで SingleOrDefault
を使用すると、次のようになります。
- (null 参照で
Courses
プロパティを見つけようとすると) 例外になります。 - 例外メッセージに問題の原因が明確に示されない場合があります。
次のコードは、コースが選択されたときにビュー モデルの Enrollments
プロパティを設定します。
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
次のマークアップを Pages/Instructors/Index.cshtml
Razor ページの末尾に追加します。
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.Instructor.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.Instructor.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
上記のマークアップは、インストラクターが選択されたときに、インストラクターに関連するコースのリストを表示します。
アプリをテストします。 Instructors ページの [Select] リンクをクリックします。
受講者データを表示する
このセクションでは、選択したコースの受講者データを表示するため、アプリが更新されます。
Pages/Instructors/Index.cshtml.cs
の OnGetAsync
メソッド内にあるクエリを次のコードで更新します。
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Pages/Instructors/Index.cshtml
を更新します。 ファイルの末尾に次のマークアップを追加します。
@if (Model.Instructor.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.Instructor.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
上記のマークアップは、選択したコースに登録されている受講者のリストを表示します。
ページを更新し、インストラクターを選択します。 コースを選択して、登録済みの受講者とその成績のリストを表示します。
Single を使用する
Single
メソッドは、Where
メソッドを別に呼び出す代わりに、Where
条件で渡すことができます。
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Single(
i => i.ID == id.Value);
Instructor.Courses = instructor.CourseAssignments.Select(
s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Single(
x => x.CourseID == courseID).Enrollments;
}
}
上記の Single
アプローチでは、Where
を使用すること以上のメリットは提供されません。 一部の開発者は、Single
アプローチ スタイルを選択します。
明示的読み込み
現在のコードは、Enrollments
と Students
に一括読み込みを指定します。
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
ユーザーがコースの登録を表示することはほとんどないとします。 その場合、最適化は要求された場合にのみ登録データを読み込むことです。 このセクションでは、Enrollments
と Students
の明示的読み込みを使用するために OnGetAsync
が更新されます。
次のコードを使用して OnGetAsync
を更新します。
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
//.Include(i => i.CourseAssignments)
// .ThenInclude(i => i.Course)
// .ThenInclude(i => i.Enrollments)
// .ThenInclude(i => i.Student)
// .AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Where(
i => i.ID == id.Value).Single();
Instructor.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
var selectedCourse = Instructor.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();
}
Instructor.Enrollments = selectedCourse.Enrollments;
}
}
上記のコードは、登録と学生データの ThenInclude メソッド呼び出しを破棄します。 コースが選択されると、強調表示されたコードが以下を取得します。
- 選択したコースの
Enrollment
エンティティ。 - 各
Enrollment
のStudent
エンティティ。
上記のコードでは、.AsNoTracking()
がコメント アウトされていることに注目してください。 追跡対象のエンティティに対して、ナビゲーション プロパティのみを明示的に読み込むことができます。
アプリをテストします。 ユーザーの観点からは、アプリの動作は以前のバージョンと同じです。
次のチュートリアルでは、関連データの更新方法を示します。
その他の技術情報
ASP.NET Core