6부. ASP.NET Core에서 EF Core를 사용한 Razor Pages - 관련 데이터 읽기

작성자: Tom Dykstra, Jon P SmithRick Anderson

Contoso University 웹앱은 EF Core 및 Visual Studio를 사용하여 Razor Pages 웹앱을 만드는 방법을 보여줍니다. 자습서 시리즈에 대한 정보는 첫 번째 자습서를 참조합니다.

해결할 수 없는 문제가 발생한 경우 완성된 앱을 다운로드하고 자습서를 따라 만든 코드와 해당 코드를 비교합니다.

이 자습서에서는 관련 데이터를 읽고 표시하는 방법을 보여 줍니다. 관련된 데이터는 EF Core에서 탐색 속성에 로드하는 데이터입니다.

다음 그림은 이 자습서에 대해 완료된 페이지를 보여 줍니다.

Courses Index page

Instructors Index page

즉시, 명시적 및 지연 로드

여러 가지 방법으로 EF Core가 관련 데이터를 엔터티의 탐색 속성에 로드할 수 있습니다.

  • 즉시 로드. 즉시 로드는 한 형식의 엔터티에 대한 쿼리가 관련 엔터티도 로드하는 경우입니다. 엔터티를 읽을 때 관련된 데이터가 검색됩니다. 이는 일반적으로 필요한 데이터를 모두 검색하는 단일 조인 쿼리를 발생시킵니다. EF Core는 일부 형식의 즉시 로드에 대해 여러 쿼리를 실행합니다. 여러 쿼리를 실행하는 것이 큰 단일 쿼리보다 더 효율적일 수 있습니다. 즉시 로드는 IncludeThenInclude 메서드로 지정됩니다.

    Eager loading example

    즉시 로드는 컬렉션 탐색이 포함된 경우 여러 쿼리를 보냅니다.

    • 주 쿼리에 대해 한 개 쿼리
    • 로드 트리에서 각 컬렉션 "에지"에 대해 한 개 쿼리
  • Load로 쿼리 구분: 별도의 쿼리로 데이터를 검색할 수 있으며 EF Core는 탐색 속성을 "수정"합니다. “수정”한다는 것은 EF Core가 탐색 속성을 자동으로 채운다는 것을 의미합니다. Load로 쿼리를 구분하는 것은 즉시 로드보다 더 명시적인 로드입니다.

    Separate queries example

    참고: EF Core는 이전에 컨텍스트 인스턴스에 로드된 다른 엔터티로 탐색 속성을 자동으로 수정합니다. 탐색 속성에 대한 데이터가 명시적으로 포함되지 않더라도 관련 엔터티의 일부 또는 전부가 이전에 로드된 경우에도 속성이 채워질 수 있습니다.

  • 명시적 로드. 엔터티를 처음 읽을 때 관련된 데이터가 검색되지 않습니다. 필요할 때 관련된 데이터를 검색하기 위한 코드를 작성해야 합니다. 별도 쿼리가 있는 명시적 로드의 경우 여러 쿼리가 데이터베이스로 전송됩니다. 명시적 로드에서 코드는 로드될 탐색 속성을 지정합니다. Load 메서드를 사용하여 명시적 로드를 수행합니다. 예시:

    Explicit loading example

  • 지연 로드. 엔터티를 처음 읽을 때 관련된 데이터가 검색되지 않습니다. 탐색 속성에 처음으로 액세스하려고 할 때 해당 탐색 속성에 필요한 데이터가 자동으로 검색됩니다. 탐색 속성에 처음으로 액세스할 때마다 쿼리가 데이터베이스에 전송됩니다. 개발자가 N+1 쿼리를 사용하는 것과 같이 지연 로드는 성능을 저해시킬 수 있습니다. N+1 쿼리는 부모를 로드하고 자식을 열거합니다.

과정 페이지 만들기

Course 엔터티는 Department 엔터티가 포함된 탐색 속성을 포함합니다.

Course.Department

과정에 대해 할당된 부서의 이름을 표시하려면 다음을 수행합니다.

  • 관련된 Department 엔터티를 Course.Department 탐색 속성에 로드합니다.
  • Department 엔터티의 Name 속성에서 이름을 가져옵니다.

과정 페이지 스캐폴드

  • 다음 예외가 포함된 학생 페이지 스캐폴드의 지침을 따릅니다.

    • Pages/Courses 폴더를 만듭니다.
    • 모델 클래스에 Course를 사용합니다.
    • 새 컨텍스트 클래스를 만드는 대신 기존 컨텍스트 클래스를 사용합니다.
  • Pages/Courses/Index.cshtml.cs을 열고 OnGetAsync 메서드를 검사합니다. 스캐폴딩 엔진은 Department 탐색 속성에 대한 즉시 로드를 지정했습니다. Include 메서드가 즉시 로드를 지정합니다.

  • 앱을 실행하고 과정 링크를 선택합니다. 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 엔터티의 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을 참조하세요.

강사 페이지 만들기

이 섹션에서는 강사 페이지를 스캐폴드하고 강사 인덱스 페이지에 관련된 과정 및 등록을 추가합니다.

Instructors Index page

이 페이지는 다음과 같은 방법으로 관련된 데이터를 읽고 표시합니다.

  • 강사 목록은 OfficeAssignment 엔터티에서 관련된 데이터를 표시합니다(이전 이미지에서 Office). InstructorOfficeAssignment 엔터티는 일대영 또는 일 관계에 있습니다. 즉시 로드는 OfficeAssignment 엔터티에 사용됩니다. 즉시 로드는 일반적으로 관련된 데이터를 표시해야 할 때 더 효율적입니다. 이 경우 강사를 위한 사무실 할당이 표시됩니다.
  • 사용자가 강사를 선택하면 관련된 Course 엔터티가 표시됩니다. InstructorCourse 엔터티는 다대다 관계에 있습니다. Course 엔터티 및 관련 Department 엔터티에 대해 즉시 로드가 사용됩니다. 이 경우 선택한 강사에 대한 과정만 필요하므로 별도 쿼리가 더 효율적일 수 있습니다. 이 예제에서는 탐색 속성에 있는 엔터티에서 탐색 속성에 대한 즉시 로드를 사용하는 방법을 보여 줍니다.
  • 사용자가 과정을 선택하면 Enrollments 엔터티의 관련 데이터가 표시됩니다. 이전 이미지에서는 학생 이름 및 학점이 표시됩니다. CourseEnrollment 엔터티는 일대다 관계에 있습니다.

뷰 모델 만들기

강사 페이지는 서로 다른 세 테이블의 데이터를 표시합니다. 세 개의 테이블을 나타내는 세 개의 속성을 포함하는 뷰 모델이 필요합니다.

다음 코드를 사용하여 만듭니 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; }
    }
}

강사 페이지 스캐폴드

  • 다음 예외가 포함된 학생 페이지 스캐폴드의 지침을 따릅니다.

    • Pages/Instructors 폴더를 만듭니다.
    • 모델 클래스에 Instructor를 사용합니다.
    • 새 컨텍스트 클래스를 만드는 대신 기존 컨텍스트 클래스를 사용합니다.

앱을 실행하고 강사 페이지로 이동합니다.

다음 코드를 사용하여 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 메서드는 컬렉션도 반환합니다. 그러나 이 경우 필터는 단일 엔터티를 선택하므로 Single 메서드를 호출하여 컬렉션을 단일 Instructor 엔터티로 변환합니다. Instructor 엔터티는 Course 탐색 속성에 대한 액세스를 제공합니다.

Single 메서드는 컬렉션에 한 개 항목만 있을 때 컬렉션에서 사용됩니다. Single 메서드는 컬렉션이 비어 있거나 둘 이상의 항목이 있는 경우 예외를 throw합니다. 대안은 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;
}

강사 인덱스 페이지 업데이트

다음 코드를 사용하여 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> |
    
  • 선택된 강사에 대한 과정 테이블을 추가합니다.

  • 선택된 과정에 대한 학생 등록 테이블을 추가합니다.

앱을 실행하고 강사 탭을 선택합니다. 페이지에 관련된 OfficeAssignment 엔터티의 Location(사무실)이 표시됩니다. OfficeAssignment가 Null이면 빈 테이블 셀이 표시됩니다.

강사의 Select 링크를 클릭합니다. 강사에게 할당된 행 스타일 변경 내용 및 과정이 표시됩니다.

과정을 선택하여 등록된 학생 및 해당 등급의 목록을 봅니다.

Instructors Index page instructor and course selected

다음 단계

다음 자습서에서는 관련된 데이터를 업데이트하는 방법을 보여 줍니다.

이 자습서에서는 관련 데이터를 읽고 표시하는 방법을 보여 줍니다. 관련된 데이터는 EF Core에서 탐색 속성에 로드하는 데이터입니다.

다음 그림은 이 자습서에 대해 완료된 페이지를 보여 줍니다.

Courses Index page

Instructors Index page

즉시, 명시적 및 지연 로드

여러 가지 방법으로 EF Core가 관련 데이터를 엔터티의 탐색 속성에 로드할 수 있습니다.

  • 즉시 로드. 즉시 로드는 한 형식의 엔터티에 대한 쿼리가 관련 엔터티도 로드하는 경우입니다. 엔터티를 읽을 때 관련된 데이터가 검색됩니다. 이는 일반적으로 필요한 데이터를 모두 검색하는 단일 조인 쿼리를 발생시킵니다. EF Core는 일부 형식의 즉시 로드에 대해 여러 쿼리를 실행합니다. 여러 쿼리를 실행하는 것이 큰 단일 쿼리보다 더 효율적일 수 있습니다. 즉시 로드는 IncludeThenInclude 메서드로 지정됩니다.

    Eager loading example

    즉시 로드는 컬렉션 탐색이 포함된 경우 여러 쿼리를 보냅니다.

    • 주 쿼리에 대해 한 개 쿼리
    • 로드 트리에서 각 컬렉션 "에지"에 대해 한 개 쿼리
  • Load로 쿼리 구분: 별도의 쿼리로 데이터를 검색할 수 있으며 EF Core는 탐색 속성을 "수정"합니다. “수정”한다는 것은 EF Core가 탐색 속성을 자동으로 채운다는 것을 의미합니다. Load로 쿼리를 구분하는 것은 즉시 로드보다 더 명시적인 로드입니다.

    Separate queries example

    참고: EF Core는 이전에 컨텍스트 인스턴스에 로드된 다른 엔터티로 탐색 속성을 자동으로 수정합니다. 탐색 속성에 대한 데이터가 명시적으로 포함되지 않더라도 관련 엔터티의 일부 또는 전부가 이전에 로드된 경우에도 속성이 채워질 수 있습니다.

  • 명시적 로드. 엔터티를 처음 읽을 때 관련된 데이터가 검색되지 않습니다. 필요할 때 관련된 데이터를 검색하기 위한 코드를 작성해야 합니다. 별도 쿼리가 있는 명시적 로드의 경우 여러 쿼리가 데이터베이스로 전송됩니다. 명시적 로드에서 코드는 로드될 탐색 속성을 지정합니다. Load 메서드를 사용하여 명시적 로드를 수행합니다. 예시:

    Explicit loading example

  • 지연 로드. 엔터티를 처음 읽을 때 관련된 데이터가 검색되지 않습니다. 탐색 속성에 처음으로 액세스하려고 할 때 해당 탐색 속성에 필요한 데이터가 자동으로 검색됩니다. 탐색 속성에 처음으로 액세스할 때마다 쿼리가 데이터베이스에 전송됩니다. 개발자가 N+1 패턴을 사용하는 경우처럼 지연 로드를 사용하면 부모를 로드하고 자식을 열거하는 성능이 저하될 수 있습니다.

과정 페이지 만들기

Course 엔터티는 Department 엔터티가 포함된 탐색 속성을 포함합니다.

Course.Department

과정에 대해 할당된 부서의 이름을 표시하려면 다음을 수행합니다.

  • 관련된 Department 엔터티를 Course.Department 탐색 속성에 로드합니다.
  • Department 엔터티의 Name 속성에서 이름을 가져옵니다.

과정 페이지 스캐폴드

  • 다음 예외가 포함된 학생 페이지 스캐폴드의 지침을 따릅니다.

    • Pages/Courses 폴더를 만듭니다.
    • 모델 클래스에 Course를 사용합니다.
    • 새 컨텍스트 클래스를 만드는 대신 기존 컨텍스트 클래스를 사용합니다.
  • Pages/Courses/Index.cshtml.cs을 열고 OnGetAsync 메서드를 검사합니다. 스캐폴딩 엔진은 Department 탐색 속성에 대한 즉시 로드를 지정했습니다. Include 메서드가 즉시 로드를 지정합니다.

  • 앱을 실행하고 과정 링크를 선택합니다. 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 엔터티의 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를 참조하세요.

강사 페이지 만들기

이 섹션에서는 강사 페이지를 스캐폴드하고 강사 인덱스 페이지에 관련된 과정 및 등록을 추가합니다.

Instructors Index page

이 페이지는 다음과 같은 방법으로 관련된 데이터를 읽고 표시합니다.

  • 강사 목록은 OfficeAssignment 엔터티에서 관련된 데이터를 표시합니다(이전 이미지에서 Office). InstructorOfficeAssignment 엔터티는 일대영 또는 일 관계에 있습니다. 즉시 로드는 OfficeAssignment 엔터티에 사용됩니다. 즉시 로드는 일반적으로 관련된 데이터를 표시해야 할 때 더 효율적입니다. 이 경우 강사를 위한 사무실 할당이 표시됩니다.
  • 사용자가 강사를 선택하면 관련된 Course 엔터티가 표시됩니다. InstructorCourse 엔터티는 다대다 관계에 있습니다. Course 엔터티 및 관련 Department 엔터티에 대해 즉시 로드가 사용됩니다. 이 경우 선택한 강사에 대한 과정만 필요하므로 별도 쿼리가 더 효율적일 수 있습니다. 이 예제에서는 탐색 속성에 있는 엔터티에서 탐색 속성에 대한 즉시 로드를 사용하는 방법을 보여 줍니다.
  • 사용자가 과정을 선택하면 Enrollments 엔터티의 관련 데이터가 표시됩니다. 이전 이미지에서는 학생 이름 및 학점이 표시됩니다. CourseEnrollment 엔터티는 일대다 관계에 있습니다.

뷰 모델 만들기

강사 페이지는 서로 다른 세 테이블의 데이터를 표시합니다. 세 개의 테이블을 나타내는 세 개의 속성을 포함하는 뷰 모델이 필요합니다.

다음 코드를 사용하여 만듭니 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; }
    }
}

강사 페이지 스캐폴드

  • 다음 예외가 포함된 학생 페이지 스캐폴드의 지침을 따릅니다.

    • Pages/Instructors 폴더를 만듭니다.
    • 모델 클래스에 Instructor를 사용합니다.
    • 새 컨텍스트 클래스를 만드는 대신 기존 컨텍스트 클래스를 사용합니다.

업데이트하기 전에 스캐폴드된 페이지의 모양을 확인하려면 앱을 실행하고 강사 페이지로 이동합니다.

다음 코드를 사용하여 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에 대해 IncludeThenInclude 메서드가 반복되는 것을 알 수 있습니다. 이 반복은 Course 엔터티의 두 가지 탐색 속성에 대해 즉시 로드를 지정하는 데 필요합니다.

다음 코드는 강사가 선택되었을 때 실행됩니다(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 속성은 해당 강사의 CourseAssignments 탐색 속성에서 Course 엔터티로 로드됩니다.

Where 메서드는 컬렉션도 반환합니다. 그러나 이 경우 필터는 단일 엔터티를 선택하므로 Single 메서드를 호출하여 컬렉션을 단일 Instructor 엔터티로 변환합니다. Instructor 엔터티는 CourseAssignments 속성에 대한 액세스를 제공합니다. CourseAssignments는 관련 Course 엔터티에 대한 액세스를 제공합니다.

Instructor-to-Courses m:M

Single 메서드는 컬렉션에 한 개 항목만 있을 때 컬렉션에서 사용됩니다. Single 메서드는 컬렉션이 비어 있거나 둘 이상의 항목이 있는 경우 예외를 throw합니다. 대안은 SingleOrDefault입니다. 컬렉션이 비어 있는 경우 기본값을 반환합니다(이 경우 Null).

다음 코드는 과정을 선택할 때 뷰 모델의 Enrollments 속성을 채웁니다.

if (courseID != null)
{
    CourseID = courseID.Value;
    var selectedCourse = InstructorData.Courses
        .Where(x => x.CourseID == courseID).Single();
    InstructorData.Enrollments = selectedCourse.Enrollments;
}

강사 인덱스 페이지 업데이트

다음 코드를 사용하여 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> |
    
  • 선택된 강사에 대한 과정 테이블을 추가합니다.

  • 선택된 과정에 대한 학생 등록 테이블을 추가합니다.

앱을 실행하고 강사 탭을 선택합니다. 페이지에 관련된 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 엔터티
  • Enrollment에 대한 Student 엔터티

앞의 코드는 .AsNoTracking()을 주석으로 처리하는 것을 알 수 있습니다. 탐색 속성은 추적된 엔터티에 대해서만 명시적으로 로드할 수 있습니다.

앱을 테스트합니다. 사용자 관점에서 앱은 이전 버전과 동일하게 동작합니다.

다음 단계

다음 자습서에서는 관련된 데이터를 업데이트하는 방법을 보여 줍니다.

이 자습서에서는 관련된 데이터를 읽고 표시합니다. 관련된 데이터는 EF Core에서 탐색 속성에 로드하는 데이터입니다.

해결할 수 없는 문제가 발생할 경우 완성된 앱을 다운로드하거나 봅니다.지침을 다운로드합니다.

다음 그림은 이 자습서에 대해 완료된 페이지를 보여 줍니다.

Courses Index page

Instructors Index page

여러 가지 방법으로 EF Core가 관련 데이터를 엔터티의 탐색 속성에 로드할 수 있습니다.

  • 즉시 로드. 즉시 로드는 한 형식의 엔터티에 대한 쿼리가 관련 엔터티도 로드하는 경우입니다. 엔터티를 읽을 때 관련된 데이터가 검색됩니다. 이는 일반적으로 필요한 데이터를 모두 검색하는 단일 조인 쿼리를 발생시킵니다. EF Core는 일부 형식의 즉시 로드에 대해 여러 쿼리를 실행합니다. 여러 쿼리를 실행하는 것이 단일 쿼리가 있는 EF6의 일부 쿼리보다 효율적일 수 있습니다. 즉시 로드는 IncludeThenInclude 메서드로 지정됩니다.

    Eager loading example

    즉시 로드는 컬렉션 탐색이 포함된 경우 여러 쿼리를 보냅니다.

    • 주 쿼리에 대해 한 개 쿼리
    • 로드 트리에서 각 컬렉션 "에지"에 대해 한 개 쿼리
  • Load로 쿼리 구분: 별도의 쿼리로 데이터를 검색할 수 있으며 EF Core는 탐색 속성을 "수정"합니다. “수정”한다는 것은 EF Core가 탐색 속성을 자동으로 채운다는 것을 의미합니다. Load로 쿼리를 구분하는 것은 즉시 로드보다 더 명시적인 로드입니다.

    Separate queries example

    참고: EF Core는 이전에 컨텍스트 인스턴스에 로드된 다른 엔터티로 탐색 속성을 자동으로 수정합니다. 탐색 속성에 대한 데이터가 명시적으로 포함되지 않더라도 관련 엔터티의 일부 또는 전부가 이전에 로드된 경우에도 속성이 채워질 수 있습니다.

  • 명시적 로드. 엔터티를 처음 읽을 때 관련된 데이터가 검색되지 않습니다. 필요할 때 관련된 데이터를 검색하기 위한 코드를 작성해야 합니다. 별도 쿼리가 있는 명시적 로드의 경우 여러 쿼리가 DB로 전송됩니다. 명시적 로드에서 코드는 로드될 탐색 속성을 지정합니다. Load 메서드를 사용하여 명시적 로드를 수행합니다. 예시:

    Explicit loading example

  • 지연 로드. 지연 로드가 버전 2.1의 EF Core에 추가되었습니다. 엔터티를 처음 읽을 때 관련된 데이터가 검색되지 않습니다. 탐색 속성에 처음으로 액세스하려고 할 때 해당 탐색 속성에 필요한 데이터가 자동으로 검색됩니다. 탐색 속성에 처음으로 액세스할 때마다 쿼리가 DB에 전송됩니다.

  • Select 연산자는 필요한 관련된 데이터만 로드합니다.

부서 이름을 표시하는 과정 페이지 만들기

과정 엔터티는 Department 엔터티가 포함된 탐색 속성을 포함합니다. Department 엔터티는 과정이 할당된 부서를 포함합니다.

과정 목록에 할당된 부서 이름을 표시하려면

  • Department 엔터티에서 Name 속성을 가져옵니다.
  • Department 엔터티는 Course.Department 탐색 속성에서 가져옵니다.

Course.Department

과정 모델 스캐폴드

학생 모델 스캐폴드의 지침을 따르고 Course를 모델 클래스로 사용합니다.

위의 명령은 Course 모델을 스캐폴드합니다. Visual Studio에서 프로젝트를 엽니다.

Pages/Courses/Index.cshtml.cs을 열고 OnGetAsync 메서드를 검사합니다. 스캐폴딩 엔진은 Department 탐색 속성에 대한 즉시 로드를 지정했습니다. Include 메서드가 즉시 로드를 지정합니다.

앱을 실행하고 과정 링크를 선택합니다. 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 엔터티의 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 Index page

이 페이지는 다음과 같은 방법으로 관련된 데이터를 읽고 표시합니다.

  • 강사 목록은 OfficeAssignment 엔터티에서 관련된 데이터를 표시합니다(이전 이미지에서 Office). InstructorOfficeAssignment 엔터티는 일대영 또는 일 관계에 있습니다. 즉시 로드는 OfficeAssignment 엔터티에 사용됩니다. 즉시 로드는 일반적으로 관련된 데이터를 표시해야 할 때 더 효율적입니다. 이 경우 강사를 위한 사무실 할당이 표시됩니다.
  • 사용자가 강사(이전 이미지에서 Harui)를 선택하는 경우 관련된 Course 엔터티가 표시됩니다. InstructorCourse 엔터티는 다대다 관계에 있습니다. Course 엔터티 및 관련 Department 엔터티에 대해 즉시 로드가 사용됩니다. 이 경우 선택한 강사에 대한 과정만 필요하므로 별도 쿼리가 더 효율적일 수 있습니다. 이 예제에서는 탐색 속성에 있는 엔터티에서 탐색 속성에 대한 즉시 로드를 사용하는 방법을 보여 줍니다.
  • 사용자가 과정(이전 이미지에서 Chemistry)을 선택하면 Enrollments 엔터티의 관련된 데이터가 표시됩니다. 이전 이미지에서는 학생 이름 및 학점이 표시됩니다. CourseEnrollment 엔터티는 일대다 관계에 있습니다.

강사 인덱스 보기에 대한 뷰 모델 만들기

강사 페이지는 서로 다른 세 테이블의 데이터를 표시합니다. 세 개 테이블을 나타내는 세 개 엔터티가 포함된 뷰 모델이 생성됩니다.

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를 모델 클래스로 사용합니다.

위의 명령은 Instructor 모델을 스캐폴드합니다. 앱을 실행하고 강사 페이지로 이동합니다.

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

쿼리에는 다음 두 include가 포함됩니다.

  • OfficeAssignment: 강사 뷰에 표시됩니다.
  • CourseAssignments: 가르친 과정을 가져옵니다.

강사 인덱스 페이지 업데이트

다음 태그를 사용하여 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 지시문이 @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> |
    

앱을 실행하고 강사 탭을 선택합니다. 페이지에 관련된 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 속성은 해당 강사의 CourseAssignments 탐색 속성에서 Course 엔터티로 로드됩니다.

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 메서드에서 단일 Instructor 엔터티만 반환됩니다. Single 메서드는 컬렉션을 단일 Instructor 엔터티로 변환합니다. Instructor 엔터티는 CourseAssignments 속성에 대한 액세스를 제공합니다. CourseAssignments는 관련 Course 엔터티에 대한 액세스를 제공합니다.

Instructor-to-Courses m:M

Single 메서드는 컬렉션에 한 개 항목만 있을 때 컬렉션에서 사용됩니다. Single 메서드는 컬렉션이 비어 있거나 둘 이상의 항목이 있는 경우 예외를 throw합니다. 대안은 SingleOrDefault입니다. 컬렉션이 비어 있는 경우 기본값을 반환합니다(이 경우 Null). 비어 있는 컬렉션에 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>
}

위의 표시는 강사가 선택된 경우 강사와 관련된 과정의 목록을 표시합니다.

앱을 테스트합니다. 강사 페이지에서 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>
}

이전 표시는 선택한 과정을 등록한 학생의 목록을 표시합니다.

페이지를 새로 고치고 강사를 선택합니다. 과정을 선택하여 등록된 학생 및 해당 등급의 목록을 봅니다.

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 엔터티
  • Enrollment에 대한 Student 엔터티

위의 코드에서는 .AsNoTracking()을 주석 처리했습니다. 탐색 속성은 추적된 엔터티에 대해서만 명시적으로 로드할 수 있습니다.

앱을 테스트합니다. 사용자 관점에서 앱은 이전 버전과 동일하게 동작합니다.

다음 자습서에서는 관련된 데이터를 업데이트하는 방법을 보여 줍니다.

추가 리소스