パート 6、ASP.NET Core の Razor ページと EF Core - 関連データの読み込み

作成者: Tom DykstraJon P SmithRick Anderson

Contoso 大学 Web アプリでは、EF Core と Visual Studio を使用して Razor Pages Web アプリを作成する方法を示します。 チュートリアル シリーズについては、最初のチュートリアルを参照してください。

解決できない問題が発生した場合は、完成したアプリをダウンロードし、チュートリアルに従って作成した内容とコードを比較します。

このチュートリアルでは、関連データを読み取って表示する方法を示します。 関連データとは、EF Core がナビゲーション プロパティに読み込むデータのことです。

以下の図は、このチュートリアルの完成したページを示しています。

Courses Index page

Instructors Index page

一括読み込み、明示的読み込み、遅延読み込み

EF Core がエンティティのナビゲーション プロパティに関連データを読み込むには、次の複数の方法があります。

  • 一括読み込み。 一括読み込みは、エンティティの 1 つの型に対するクエリが関連エンティティも読み込む場合です。 エンティティが読み取られるときに、その関連データが取得されます。 これは通常、必要なデータをすべて取得する 1 つの結合クエリになります。 EF Core は、一部の型の一括読み込みに対して複数のクエリを発行します。 複数のクエリを発行する方が、1 つの大規模なクエリよりも効率的である場合があります。 一括読み込みは、Include メソッドと ThenInclude メソッドを使用して指定されます。

    Eager loading example

    一括読み込みでは、コレクション ナビゲーションが含まれるときに、複数のクエリが送信されます。

    • メイン クエリに 1 つのクエリ
    • 読み込みツリー内のコレクション "エッジ" ごとに 1 つのクエリ
  • Load で分離したクエリ: データは分離したクエリで取得でき、EF Core がナビゲーション プロパティを "修正" します。 "修正" は、ナビゲーション プロパティが EF Core によって自動的に入力されることを意味します。 Load で分離したクエリは、一括読み込みよりも明示的読み込みに似ています。

    Separate queries example

    注:EF Core は、コンテキスト インスタンスに以前に読み込まれたその他のエンティティに対して、ナビゲーション プロパティを自動的に修正します。 ナビゲーション プロパティのデータが明示的に含まれない場合でも、関連エンティティの一部またはすべてが以前に読み込まれていれば、プロパティを設定することができます。

  • 明示的読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 必要なときに関連するデータを取得するコードを記述する必要があります。 分離したクエリによる明示的読み込みにより、複数のクエリがデータベースに送信されます。 明示的読み込みでは、コードで読み込まれるナビゲーション プロパティを指定します。 明示的読み込みを行うには、Load メソッドを使用します。 次に例を示します。

    Explicit loading example

  • 遅延読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 ナビゲーション プロパティに初めてアクセスすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティにアクセスされるたびに、クエリがデータベースに送信されます。 遅延読み込みでは、開発者が N + 1 クエリを使用する場合などにパフォーマンスが低下する可能性があります。 N + 1 クエリは親を読み込んで子を列挙します。

Course ページの作成

Course エンティティには、関連する Department エンティティを含むナビゲーション プロパティが含まれています。

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] タブを選択して部門名のリストを表示します。

Courses Index page

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 ページに追加します。

Instructors Index page

このページは、次の方法で関連データを読み取って表示します。

  • インストラクターのリストには 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 リンクをクリックします。 行スタイルの変更とそのインストラクターに割り当てられたコースが表示されます。

コースを選択して、登録済みの受講者とその成績のリストを表示します。

Instructors Index page instructor and course selected

次のステップ

次のチュートリアルでは、関連データの更新方法を示します。

このチュートリアルでは、関連データを読み取って表示する方法を示します。 関連データとは、EF Core がナビゲーション プロパティに読み込むデータのことです。

以下の図は、このチュートリアルの完成したページを示しています。

Courses Index page

Instructors Index page

一括読み込み、明示的読み込み、遅延読み込み

EF Core がエンティティのナビゲーション プロパティに関連データを読み込むには、次の複数の方法があります。

  • 一括読み込み。 一括読み込みは、エンティティの 1 つの型に対するクエリが関連エンティティも読み込む場合です。 エンティティが読み取られるときに、その関連データが取得されます。 これは通常、必要なデータをすべて取得する 1 つの結合クエリになります。 EF Core は、一部の型の一括読み込みに対して複数のクエリを発行します。 複数のクエリを発行することは、1 つの巨大なクエリよりも効率的である可能性があります。 一括読み込みは、Include メソッドと ThenInclude メソッドを使用して指定されます。

    Eager loading example

    一括読み込みでは、コレクション ナビゲーションが含まれるときに、複数のクエリが送信されます。

    • メイン クエリに 1 つのクエリ
    • 読み込みツリー内のコレクション "エッジ" ごとに 1 つのクエリ
  • Load で分離したクエリ: データは分離したクエリで取得でき、EF Core がナビゲーション プロパティを "修正" します。 "修正" は、ナビゲーション プロパティが EF Core によって自動的に入力されることを意味します。 Load で分離したクエリは、一括読み込みよりも明示的読み込みに似ています。

    Separate queries example

    注:EF Core は、コンテキスト インスタンスに以前に読み込まれたその他のエンティティに対して、ナビゲーション プロパティを自動的に修正します。 ナビゲーション プロパティのデータが明示的に含まれない場合でも、関連エンティティの一部またはすべてが以前に読み込まれていれば、プロパティを設定することができます。

  • 明示的読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 必要なときに関連するデータを取得するコードを記述する必要があります。 分離したクエリによる明示的読み込みにより、複数のクエリがデータベースに送信されます。 明示的読み込みでは、コードで読み込まれるナビゲーション プロパティを指定します。 明示的読み込みを行うには、Load メソッドを使用します。 次に例を示します。

    Explicit loading example

  • 遅延読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 ナビゲーション プロパティに初めてアクセスすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティにアクセスされるたびに、クエリがデータベースに送信されます。 開発者が N + 1 パターンを使用して、親を読み込んで子を列挙する場合など、遅延読み込みでパフォーマンスが低下することがあります。

Course ページの作成

Course エンティティには、関連する Department エンティティを含むナビゲーション プロパティが含まれています。

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] タブを選択して部門名のリストを表示します。

Courses Index page

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.cshtmlIndexSelect.cshtml.cs を参照してください。

Instructor ページの作成

このセクションでは、Instructor ページをスキャフォールディングし、関連する Courses と Enrollments を Instructors Index ページに追加します。

Instructors Index page

このページは、次の方法で関連データを読み取って表示します。

  • インストラクターのリストには 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

CourseAssignmentsCourse に対する 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 エンティティへのアクセスを提供します。

Instructor-to-Courses m:M

コレクションに 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 リンクをクリックします。 行スタイルの変更とそのインストラクターに割り当てられたコースが表示されます。

コースを選択して、登録済みの受講者とその成績のリストを表示します。

Instructors Index page instructor and course selected

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 メソッドを使用しても利点はありません。

明示的読み込み

現在のコードは、EnrollmentsStudents に一括読み込みを指定します。

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();

ユーザーがコースの登録を表示することはほとんどないとします。 その場合、最適化は要求された場合にのみ登録データを読み込むことです。 このセクションでは、EnrollmentsStudents の明示的読み込みを使用するために 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 エンティティ。
  • EnrollmentStudent エンティティ。

上記のコードでは、.AsNoTracking() がコメント アウトされていることに注目してください。 追跡対象のエンティティに対して、ナビゲーション プロパティのみを明示的に読み込むことができます。

アプリをテストします。 ユーザーの観点からは、アプリの動作は以前のバージョンと同じです。

次の手順

次のチュートリアルでは、関連データの更新方法を示します。

このチュートリアルでは、関連データが読み取られ、表示されます。 関連データとは、EF Core がナビゲーション プロパティに読み込むデータのことです。

解決できない問題が発生した場合は、完成したアプリをダウンロードまたは表示してください手順をダウンロードする

以下の図は、このチュートリアルの完成したページを示しています。

Courses Index page

Instructors Index page

EF Core がエンティティのナビゲーション プロパティに関連データを読み込むには、次の複数の方法があります。

  • 一括読み込み。 一括読み込みは、エンティティの 1 つの型に対するクエリが関連エンティティも読み込む場合です。 エンティティが読み取られるときに、その関連データが取得されます。 これは通常、必要なすべてのデータを取得する 1 つの結合クエリになります。 EF Core は、一部の型の一括読み込みに対して複数のクエリを発行します。 複数のクエリを発行することで、1 つのクエリしかなかった EF6 の一部のクエリよりも、効率を高めることができます。 一括読み込みは、Include メソッドと ThenInclude メソッドを使用して指定されます。

    Eager loading example

    一括読み込みでは、コレクション ナビゲーションが含まれるときに、複数のクエリが送信されます。

    • メイン クエリに 1 つのクエリ
    • 読み込みツリー内のコレクション "エッジ" ごとに 1 つのクエリ
  • Load で分離したクエリ: データは分離したクエリで取得でき、EF Core がナビゲーション プロパティを "修正" します。 "修正" は、ナビゲーション プロパティが EF Core によって自動的に入力されることを意味します。 Load で分離したクエリは、一括読み込みよりも明示的読み込みに似ています。

    Separate queries example

    注: EF Core は、コンテキスト インスタンスに以前に読み込まれたその他のエンティティに対して、ナビゲーション プロパティを自動的に修正します。 ナビゲーション プロパティのデータが明示的に含まれない場合でも、関連エンティティの一部またはすべてが以前に読み込まれていれば、プロパティを設定することができます。

  • 明示的読み込み。 エンティティが最初に読み込まれるときに、関連データは取得されません。 必要なときに関連するデータを取得するコードを記述する必要があります。 分離したクエリによる明示的読み込みにより、複数のクエリが DB に送信されます。 明示的読み込みでは、コードで読み込まれるナビゲーション プロパティを指定します。 明示的読み込みを行うには、Load メソッドを使用します。 次に例を示します。

    Explicit loading example

  • 遅延読み込み遅延読み込みがバージョン 2.1 の EF Core に追加されました。 エンティティが最初に読み込まれるときに、関連データは取得されません。 ナビゲーション プロパティに初めてアクセスすると、そのナビゲーション プロパティに必要なデータが自動的に取得されます。 初めてナビゲーション プロパティにアクセスされるたびに、クエリが DB に送信されます。

  • Select 演算子は必要な関連データのみを読み込みます。

部門名を表示する Course ページを作成する

Course エンティティには、Department エンティティを含むナビゲーション プロパティが含まれています。 Department エンティティには、コースが割り当てられる部門が含まれています。

コースの一覧で割り当てられている部門の名前を表示するには:

  • Department エンティティから Name プロパティを取得します。
  • Department エンティティは Course.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] タブを選択して部門名のリストを表示します。

Courses Index page

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.cshtmlIndexSelect.cshtml.cs を参照してください。

コース登録を示す Instructors ページを作成する

このセクションでは、Instructors ページが作成されます。

Instructors Index page

このページは、次の方法で関連データを読み取って表示します。

  • インストラクターのリストには 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.csOnGetAsync メソッドを次のコードで更新します。

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 エンティティへのアクセスを提供します。

Instructor-to-Courses m:M

コレクションに 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.cshtmlRazor ページの末尾に追加します。

                    <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.csOnGetAsync メソッド内にあるクエリを次のコードで更新します。

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>
}

上記のマークアップは、選択したコースに登録されている受講者のリストを表示します。

ページを更新し、インストラクターを選択します。 コースを選択して、登録済みの受講者とその成績のリストを表示します。

Instructors Index page instructor and course selected

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 アプローチ スタイルを選択します。

明示的読み込み

現在のコードは、EnrollmentsStudents に一括読み込みを指定します。

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();

ユーザーがコースの登録を表示することはほとんどないとします。 その場合、最適化は要求された場合にのみ登録データを読み込むことです。 このセクションでは、EnrollmentsStudents の明示的読み込みを使用するために 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 エンティティ。
  • EnrollmentStudent エンティティ。

上記のコードでは、.AsNoTracking() がコメント アウトされていることに注目してください。 追跡対象のエンティティに対して、ナビゲーション プロパティのみを明示的に読み込むことができます。

アプリをテストします。 ユーザーの観点からは、アプリの動作は以前のバージョンと同じです。

次のチュートリアルでは、関連データの更新方法を示します。

その他の技術情報