Часть 6. Razor Страницы с EF Core ASP.NET Core — чтение связанных данных
Авторы: Том Дайкстра (Tom Dykstra), Йон П. Смит (Jon P Smith) и Рик Андерсон (Rick Anderson)
Веб-приложение Contoso University демонстрирует создание Razor веб-приложений Pages с помощью EF Core Visual Studio. Сведения о серии руководств см. в первом руководстве серии.
При возникновении проблем, которые вам не удается устранить, скачайте готовое приложение и сравните его код с тем, который вы создали в процессе работы с этим руководством.
Этот учебник посвящен чтению и отображению связанных данных. Связанные данные — это данные, которые EF Core загружаются в свойства навигации.
На следующих рисунках изображены готовые страницы для этого руководства:
Безотложная, явная и отложенная загрузка
Существует несколько способов EF Core загрузки связанных данных в свойства навигации сущности:
Безотложная загрузка. Безотложной является загрузка, когда запрос для одного типа сущности также загружает связанные сущности. При чтении сущности извлекаются ее связанные данные. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. EF Core будет выдавать несколько запросов для некоторых типов охотной загрузки. Отправка нескольких запросов может оказаться эффективнее, чем выполнение одного большого запроса. Безотложная загрузка указывается с помощью методов Include и ThenInclude.
Безотложная загрузка отправляет несколько запросов, когда включена навигация коллекции:
- Один запрос для основного запроса
- Один запрос для каждого "края" коллекции в дереве загрузки.
Отдельные запросы с
Load
: данные можно получить в отдельных запросах и EF Core "исправляет" свойства навигации. "Исправление" означает, что EF Core автоматически заполняет свойства навигации. Отдельные запросы сLoad
больше похожи на явную загрузку, чем на безотложную.Примечание.EF Core Автоматически исправляет свойства навигации для любых других сущностей, которые ранее были загружены в экземпляр контекста. Даже если данные для свойства навигации не включены явно, свойство все равно можно заполнить при условии, что ранее были загружены некоторые или все связанные сущности.
Явная загрузка. При первом чтении сущности связанные данные не извлекаются. Нужно написать код, извлекающий связанные данные, когда они необходимы. Явная загрузка с отдельными запросами приводит к отправке нескольких запросов к базе данных. При явной загрузке код указывает, какие свойства навигации нужно загрузить. Для выполнения явной загрузки используется метод
Load
. Например:Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. При первом обращении к свойству навигации необходимые для этого свойства данные извлекаются автоматически. Запрос к базе данных отправляется при каждом первом обращении к свойству навигации. Отложенная загрузка может снизить производительность, например когда разработчики используют запросы N+1, загружая родительский объект и перечисляя дочерние объекты.
Создание страниц курсов
Сущность Course
включает в себя свойство навигации, содержащее связанную сущность Department
.
Чтобы отобразить имя кафедры, которой назначен курс, выполните указанные ниже действия.
- Загрузите связанную сущность
Department
в свойство навигацииCourse.Department
. - Получите имя из свойства
Name
сущностиDepartment
.
Формирование шаблона для страниц курсов
Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:
- Создайте папку Pages/Courses.
- Используйте класс модели
Course
. - Используйте существующий класс контекста вместо создания нового.
Откройте
Pages/Courses/Index.cshtml.cs
и проверьте методOnGetAsync
. Подсистема формирования шаблонов указала безотложную загрузку для свойства навигацииDepartment
. МетодInclude
задает безотложную загрузку.Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры отображается бесполезный
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
.Добавлен столбец Number (Номер), отображающий значение свойства
CourseID
. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл.Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство
Name
сущностиDepartment
, которая загружена в свойство навигацииDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Для просмотра списка с названиями кафедр запустите приложение и выберите вкладку Courses (Курсы).
Загрузка связанных данных с помощью "Select"
Метод OnGetAsync
загружает связанные данные с помощью метода Include
. Метод Select
является альтернативным вариантом, который загружает только необходимые связанные данные. Для отдельных элементов, таких как Department.Name
, используется SQL INNER JOIN
. Для коллекций используется доступ к другой базе данных, но это же делает и оператор Include
в коллекциях.
Следующий код загружает связанные данные с помощью метода Select
:
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
Приведенный выше код не возвращает никаких типов сущностей, поэтому отслеживание не выполняется. Дополнительные сведения об отслеживании EF см. в разделе "Отслеживание" и "Запросы без отслеживания".
CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
Полное решение Razor Pages см. здесь.
Создание страниц преподавателей
В этом разделе формируется шаблон для страниц преподавателей, а затем на страницу указателя преподавателей добавляются связанные курсы и регистрации.
Эта страница считывает и отображает связанные данные следующим образом:
- Список преподавателей отображает связанные данные из сущности
OfficeAssignment
("Office" (Кабинет) на предыдущем изображении). Между сущностямиInstructor
иOfficeAssignment
действует связь один к нулю или к одному. Безотложная загрузка используется для сущностейOfficeAssignment
. Безотложная загрузка обычно более эффективна, когда требуется отобразить связанные данные. В этом случае отображается принадлежность к кабинету для каждого преподавателя. - Когда пользователь выбирает преподавателя, отображаются связанные сущности
Course
. Между сущностямиInstructor
иCourse
действует связь многие ко многим. Используется безотложная загрузка для сущностейCourse
и связанных сущностейDepartment
. В этом случае отдельные запросы могут оказаться эффективнее, так как требуются только курсы для выбранного преподавателя. Этот пример показывает, как использовать безотложную загрузку для свойств навигации в сущностях, находящихся в свойствах навигации. - Когда пользователь выбирает курс, отображаются связанные данные из сущности
Enrollments
. На предыдущем изображении отображается имя и оценка учащегося. Между сущностямиCourse
иEnrollment
действует связь один ко многим.
Создание модели представления
На странице "Instructors" (Преподаватели) отображаются данные из трех различных таблиц. Требуется модель представления, которая включает в себя три свойства, представляющие три таблицы.
Создайте 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; }
}
}
Формирование шаблона для страниц преподавателей
Следуйте инструкциям в разделе Формирование шаблона для страниц 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
принимает необязательные данные маршрутизации для идентификатора выбранного преподавателя.
Проверьте запрос в 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
вместе с сущностями Course
загружается из свойства навигации Courses
выбранного преподавателя.
Метод Where
возвращает коллекцию. В этом случае фильтр выберет одну сущность, поэтому вызывается метод Single
для преобразования коллекции в одну сущность Instructor
. Сущность Instructor
предоставляет доступ к свойству навигации Course
.
Метод 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;
}
Изменение страницы индекса преподавателей
Обновите 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-адресе для маршрутизации данных. Например, при выборе ссылки Select для преподавателя только с директивой@page
формируется URL-адрес следующего вида:https://localhost:5001/Instructors?id=2
Когда используется директива страницы
@page "{id:int?}"
, URL-адрес имеет следующее значение:https://localhost:5001/Instructors/2
.Добавляет столбец Office, в котором отображается
item.OfficeAssignment.Location
только тогда, когдаitem.OfficeAssignment
не равно NULL. Так как это связь один к нулю или одному, то связанная сущность OfficeAssignment может отсутствовать.@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Добавляет столбец Courses, содержащий курсы, которые ведет конкретный преподаватель. Дополнительные сведения об этом razor синтаксисе см. в разделе "Явный переход строк".
Добавляет код, который динамически добавляет
class="table-success"
к элементуtr
выбранного преподавателя и курса. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "table-success"; } <tr class="@selectedRow">
Добавляет новую гиперссылку с меткой Select (Выбрать). Она отправляет идентификатор выбранного преподавателя в метод
Index
и задает цвет фона.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Добавляет таблицу курсов для выбранного преподавателя.
Добавляет таблицу регистраций учащихся для выбранного курса.
Запустите приложение и выберите вкладку Instructors. На странице отображается Location
(office) из связанной сущности OfficeAssignment
. Если OfficeAssignment
имеет значение NULL, отображается пустая ячейка таблицы.
Щелкните ссылку Select для преподавателя. Стиль строки изменится, и отобразятся курсы, назначенные этому преподавателю.
Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.
Следующие шаги
Следующее руководство посвящено обновлению связанных данных.
Этот учебник посвящен чтению и отображению связанных данных. Связанные данные — это данные, которые EF Core загружаются в свойства навигации.
На следующих рисунках изображены готовые страницы для этого руководства:
Безотложная, явная и отложенная загрузка
Существует несколько способов EF Core загрузки связанных данных в свойства навигации сущности:
Безотложная загрузка. Безотложной является загрузка, когда запрос для одного типа сущности также загружает связанные сущности. При чтении сущности извлекаются ее связанные данные. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. EF Core будет выдавать несколько запросов для некоторых типов охотной загрузки. Отправка нескольких запросов может оказаться эффективнее, чем выполнение одного большого запроса. Безотложная загрузка указывается с помощью методов
Include
иThenInclude
.Безотложная загрузка отправляет несколько запросов, когда включена навигация коллекции:
- Один запрос для основного запроса
- Один запрос для каждого "края" коллекции в дереве загрузки.
Отдельные запросы с
Load
: данные можно получить в отдельных запросах и EF Core "исправляет" свойства навигации. "Исправление" означает, что EF Core автоматически заполняет свойства навигации. Отдельные запросы сLoad
больше похожи на явную загрузку, чем на безотложную.Примечание.EF Core Автоматически исправляет свойства навигации для любых других сущностей, которые ранее были загружены в экземпляр контекста. Даже если данные для свойства навигации не включены явно, свойство все равно можно заполнить при условии, что ранее были загружены некоторые или все связанные сущности.
Явная загрузка. При первом чтении сущности связанные данные не извлекаются. Нужно написать код, извлекающий связанные данные, когда они необходимы. Явная загрузка с отдельными запросами приводит к отправке нескольких запросов к базе данных. При явной загрузке код указывает, какие свойства навигации нужно загрузить. Для выполнения явной загрузки используется метод
Load
. Например:Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. При первом обращении к свойству навигации необходимые для этого свойства данные извлекаются автоматически. Запрос к базе данных отправляется при каждом первом обращении к свойству навигации. Отложенная загрузка может снизить производительность, например когда разработчики используют шаблоны N+1, загружая родительский объект и перечисляя дочерние объекты.
Создание страниц курсов
Сущность Course
включает в себя свойство навигации, содержащее связанную сущность Department
.
Чтобы отобразить имя кафедры, которой назначен курс, выполните указанные ниже действия.
- Загрузите связанную сущность
Department
в свойство навигацииCourse.Department
. - Получите имя из свойства
Name
сущностиDepartment
.
Формирование шаблона для страниц курсов
Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:
- Создайте папку Pages/Courses.
- Используйте класс модели
Course
. - Используйте существующий класс контекста вместо создания нового.
Откройте
Pages/Courses/Index.cshtml.cs
и проверьте методOnGetAsync
. Подсистема формирования шаблонов указала безотложную загрузку для свойства навигацииDepartment
. МетодInclude
задает безотложную загрузку.Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры отображается бесполезный
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
.Добавлен столбец Number (Номер), отображающий значение свойства
CourseID
. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл.Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство
Name
сущностиDepartment
, которая загружена в свойство навигацииDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Для просмотра списка с названиями кафедр запустите приложение и выберите вкладку Courses (Курсы).
Загрузка связанных данных с помощью "Select"
Метод OnGetAsync
загружает связанные данные с помощью метода Include
. Метод Select
является альтернативным вариантом, который загружает только необходимые связанные данные. Для отдельных элементов, таких как Department.Name
, используется SQL INNER JOIN. Для коллекций используется доступ к другой базе данных, но это же делает и оператор Include
в коллекциях.
Следующий код загружает связанные данные с помощью метода Select
:
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
Приведенный выше код не возвращает никаких типов сущностей, поэтому отслеживание не выполняется. Дополнительные сведения об отслеживании EF см. в разделе "Отслеживание" и "Запросы без отслеживания".
CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
Полный пример см. в описании IndexSelect.cshtml и IndexSelect.cshtml.cs.
Создание страниц преподавателей
В этом разделе формируется шаблон для страниц преподавателей, а затем на страницу указателя преподавателей добавляются связанные курсы и регистрации.
Эта страница считывает и отображает связанные данные следующим образом:
- Список преподавателей отображает связанные данные из сущности
OfficeAssignment
("Office" (Кабинет) на предыдущем изображении). Между сущностямиInstructor
иOfficeAssignment
действует связь один к нулю или к одному. Безотложная загрузка используется для сущностейOfficeAssignment
. Безотложная загрузка обычно более эффективна, когда требуется отобразить связанные данные. В этом случае отображается принадлежность к кабинету для каждого преподавателя. - Когда пользователь выбирает преподавателя, отображаются связанные сущности
Course
. Между сущностямиInstructor
иCourse
действует связь многие ко многим. Используется безотложная загрузка для сущностейCourse
и связанных сущностейDepartment
. В этом случае отдельные запросы могут оказаться эффективнее, так как требуются только курсы для выбранного преподавателя. Этот пример показывает, как использовать безотложную загрузку для свойств навигации в сущностях, находящихся в свойствах навигации. - Когда пользователь выбирает курс, отображаются связанные данные из сущности
Enrollments
. На предыдущем изображении отображается имя и оценка учащегося. Между сущностямиCourse
иEnrollment
действует связь один ко многим.
Создание модели представления
На странице "Instructors" (Преподаватели) отображаются данные из трех различных таблиц. Требуется модель представления, которая включает в себя три свойства, представляющие три таблицы.
Создайте 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; }
}
}
Формирование шаблона для страниц преподавателей
Следуйте инструкциям в разделе Формирование шаблона для страниц Student, за исключением следующего:
- Создайте папку 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
принимает необязательные данные маршрутизации для идентификатора выбранного преподавателя.
Проверьте запрос в 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
Обратите внимание на то, что методы Include
и ThenInclude
повторяются для CourseAssignments
и Course
. Это необходимо для того, чтобы задать безотложную загрузку для двух свойств навигации сущности 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);
}
Выбранный преподаватель извлекается из списка преподавателей в модели представления. Из свойства навигации CourseAssignments
этого преподавателя загружается свойство модели представления Courses
вместе с сущностями Course
.
Метод Where
возвращает коллекцию. Но в этом случае фильтр выберет одну сущность, поэтому вызывается метод Single
для преобразования коллекции в одну сущность Instructor
. Сущность Instructor
предоставляет доступ к свойству CourseAssignments
. CourseAssignments
предоставляет доступ к связанным сущностям Course
.
Метод Single
используется для коллекции, когда она содержит всего один элемент. Метод Single
вызывает исключение, если коллекция пуста или содержит больше одного элемента. Альтернативным вариантом является метод 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-адресе для маршрутизации данных. Например, при выборе ссылки Select для преподавателя только с директивой@page
формируется URL-адрес следующего вида:https://localhost:5001/Instructors?id=2
Когда используется директива страницы
@page "{id:int?}"
, URL-адрес имеет следующее значение:https://localhost:5001/Instructors/2
Добавляет столбец Office, в котором отображается
item.OfficeAssignment.Location
только тогда, когдаitem.OfficeAssignment
не равно NULL. Так как это связь один к нулю или одному, то связанная сущность OfficeAssignment может отсутствовать.@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Добавляет столбец Courses, содержащий курсы, которые ведет конкретный преподаватель. Дополнительные сведения об этом razor синтаксисе см. в разделе "Явный переход строк".
Добавляет код, который динамически добавляет
class="table-success"
к элементуtr
выбранного преподавателя и курса. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "table-success"; } <tr class="@selectedRow">
Добавляет новую гиперссылку с меткой Select (Выбрать). Она отправляет идентификатор выбранного преподавателя в метод
Index
и задает цвет фона.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Добавляет таблицу курсов для выбранного преподавателя.
Добавляет таблицу регистраций учащихся для выбранного курса.
Запустите приложение и выберите вкладку Instructors. На странице отображается Location
(office) из связанной сущности OfficeAssignment
. Если OfficeAssignment
имеет значение NULL, отображается пустая ячейка таблицы.
Щелкните ссылку Select для преподавателя. Стиль строки изменится, и отобразятся курсы, назначенные этому преподавателю.
Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.
Использование метода Single
Метод Single
может передать условие Where
вместо отдельного вызова метода Where
:
public async Task OnGetAsync(int? id, int? courseID)
{
InstructorData = new InstructorIndexData();
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = InstructorData.Instructors.Single(
i => i.ID == id.Value);
InstructorData.Courses = instructor.CourseAssignments.Select(
s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
InstructorData.Enrollments = InstructorData.Courses.Single(
x => x.CourseID == courseID).Enrollments;
}
}
Использование Single
с условием WHERE — это вопрос личных предпочтений. Оно не дает преимуществ по сравнению с использованием метода Where
.
Явная загрузка
Текущий код указывает упреждающую загрузку для Enrollments
и Students
:
InstructorData.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Предположим, что пользователям редко требуется просматривать зачисления на курс. В этом случае можно оптимизировать работу, загружая данные о зачислении только при их запросе. В этом разделе изменяется OnGetAsync
для использования явной загрузки Enrollments
и Students
.
Обновите 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
для выбранного курса. - Сущности
Student
для каждогоEnrollment
.
Обратите внимание, что предыдущий код закомментирует .AsNoTracking()
. Для отслеживаемых сущностей свойства навигации можно загрузить лишь явно.
Тестирование приложения. С точки зрения пользователя приложение работает аналогично предыдущей версии.
Следующие шаги
Следующее руководство посвящено обновлению связанных данных.
В этом руководстве выполняется чтение и отображение связанных данных. Связанные данные — это данные, которые EF Core загружаются в свойства навигации.
При возникновении проблем, которые не удается решить, скачайте или просмотрите готовое приложение. Инструкции по скачиванию.
На следующих рисунках изображены готовые страницы для этого руководства:
Безотложная, явная и отложенная загрузка связанных данных
Существует несколько способов EF Core загрузки связанных данных в свойства навигации сущности:
Безотложная загрузка. Безотложной является загрузка, когда запрос для одного типа сущности также загружает связанные сущности. При чтении сущности связанные данные извлекаются. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. EF Core будет выдавать несколько запросов для некоторых типов охотной загрузки. Выдача нескольких запросов может оказаться более эффективной по сравнению с отдельными ситуациями в EF6, когда выполнялся отдельный запрос. Безотложная загрузка указывается с помощью методов
Include
иThenInclude
.Безотложная загрузка отправляет несколько запросов, когда включена навигация коллекции:
- Один запрос для основного запроса
- Один запрос для каждого "края" коллекции в дереве загрузки.
Отдельные запросы с
Load
: данные можно получить в отдельных запросах и EF Core "исправляет" свойства навигации. "Исправление" означает, что EF Core автоматически заполняет свойства навигации. Отдельные запросы сLoad
больше похожи на явную загрузку, чем на безотложную.Примечание. EF Core Автоматически исправляет свойства навигации для любых других сущностей, которые ранее были загружены в экземпляр контекста. Даже если данные для свойства навигации не включены явно, свойство все равно можно заполнить при условии, что ранее были загружены некоторые или все связанные сущности.
Явная загрузка. При первом чтении сущности связанные данные не извлекаются. Нужно написать код, извлекающий связанные данные, когда они необходимы. Явная загрузка с отдельными запросами приводит к отправке нескольких запросов к базе данных. При явной загрузке код указывает, какие свойства навигации нужно загрузить. Для выполнения явной загрузки используется метод
Load
. Например:Отложенная загрузка. Отложенная загрузка была добавлена EF Core в версию 2.1. При первом чтении сущности связанные данные не извлекаются. При первом обращении к свойству навигации необходимые для этого свойства данные извлекаются автоматически. Запрос к базе данных отправляется при каждом первом обращении к свойству навигации.
Оператор
Select
загружает только необходимые связанные данные.
Создание страницы "Course" (Курс) с отображением названий кафедр
Сущность "Course" включает в себя свойство навигации, содержащее сущность Department
. Сущность Department
содержит кафедру, которой назначен курс.
Отображение имени назначенной кафедры в списке курсов:
- Получите свойство
Name
из сущностиDepartment
. - Сущность
Department
получается из свойства навигацииCourse.Department
.
Формирование шаблона для модели Course
Следуйте инструкциям в разделе Формирование шаблона для модели Student и используйте Course
для класса модели.
Предыдущая команда формирует шаблон для модели Course
. Откройте проект в Visual Studio.
Откройте Pages/Courses/Index.cshtml.cs
и проверьте метод OnGetAsync
. Подсистема формирования шаблонов указала безотложную загрузку для свойства навигации Department
. Метод Include
задает безотложную загрузку.
Запустите приложение и выберите ссылку Courses (Курсы). В столбце кафедры отображается бесполезный 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 (Курсы).
Добавлен столбец Number (Номер), отображающий значение свойства
CourseID
. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл.Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство
Name
сущностиDepartment
, которая загружена в свойство навигацииDepartment
:@Html.DisplayFor(modelItem => item.Department.Name)
Для просмотра списка с названиями кафедр запустите приложение и выберите вкладку Courses (Курсы).
Загрузка связанных данных с помощью "Select"
Метод OnGetAsync
загружает связанные данные с помощью метода Include
:
public async Task OnGetAsync()
{
Course = await _context.Courses
.Include(c => c.Department)
.AsNoTracking()
.ToListAsync();
}
Оператор Select
загружает только необходимые связанные данные. Для отдельных элементов, таких как Department.Name
, используется SQL INNER JOIN. Для коллекций используется доступ к другой базе данных, но это же делает и оператор Include
в коллекциях.
Следующий код загружает связанные данные с помощью метода Select
:
public IList<CourseViewModel> CourseVM { get; set; }
public async Task OnGetAsync()
{
CourseVM = await _context.Courses
.Select(p => new CourseViewModel
{
CourseID = p.CourseID,
Title = p.Title,
Credits = p.Credits,
DepartmentName = p.Department.Name
}).ToListAsync();
}
CourseViewModel
:
public class CourseViewModel
{
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public string DepartmentName { get; set; }
}
Полный пример см. в описании IndexSelect.cshtml и IndexSelect.cshtml.cs.
Создание страницы "Instructors" (Преподаватели) с отображением курсов и дат зачисления
В этом разделе создается страница "Instructors" (Преподаватели).
Эта страница считывает и отображает связанные данные следующим образом:
- Список преподавателей отображает связанные данные из сущности
OfficeAssignment
("Office" (Кабинет) на предыдущем изображении). Между сущностямиInstructor
иOfficeAssignment
действует связь один к нулю или к одному. Безотложная загрузка используется для сущностейOfficeAssignment
. Безотложная загрузка обычно более эффективна, когда требуется отобразить связанные данные. В этом случае отображается принадлежность к кабинету для каждого преподавателя. - Когда пользователь выбирает преподавателя (Harui на предыдущем изображении), отображаются связанные сущности
Course
. Между сущностямиInstructor
иCourse
действует связь многие ко многим. Используется безотложная загрузка для сущностейCourse
и связанных сущностейDepartment
. В этом случае отдельные запросы могут оказаться эффективнее, так как требуются только курсы для выбранного преподавателя. Этот пример показывает, как использовать безотложную загрузку для свойств навигации в сущностях, находящихся в свойствах навигации. - Когда пользователь выбирает курс ("Chemistry" (Химия) на предыдущем изображении), отображаются связанные данные из сущности
Enrollments
. На предыдущем изображении отображается имя и оценка учащегося. Между сущностямиCourse
иEnrollment
действует связь один ко многим.
Создание модели для представления индекса преподавателей
На странице "Instructors" (Преподаватели) отображаются данные из трех различных таблиц. Создается модель представления, которая включает три сущности, представляющие три таблицы.
В папке 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
.
Запустите приложение и перейдите на страницу преподавателей.
Замените 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
принимает необязательные данные маршрутизации для идентификатора выбранного преподавателя.
Проверьте запрос в 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();
Запрос имеет две операции включения:
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-адресе для маршрутизации данных. Например, при выборе ссылки Select для преподавателя только с директивой@page
формируется URL-адрес следующего вида:http://localhost:1234/Instructors?id=2
Когда используется директива страницы
@page "{id:int?}"
, предыдущий URL-адрес имеет следующее значение:http://localhost:1234/Instructors/2
Заголовком страницы является Instructors.
Добавили столбец Office, отображающий
item.OfficeAssignment.Location
только тогда, когдаitem.OfficeAssignment
не равно null. Так как это связь один к нулю или одному, то связанная сущность OfficeAssignment может отсутствовать.@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
Добавили столбец Courses, отображающий курсы, которые ведет конкретный преподаватель. Дополнительные сведения об этом razor синтаксисе см. в разделе "Явный переход строк".
Добавили код, который динамически добавляет
class="success"
к элементуtr
выбранного преподавателя. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.string selectedRow = ""; if (item.CourseID == Model.CourseID) { selectedRow = "success"; } <tr class="@selectedRow">
Добавлена новая гиперссылка с меткой Select (Выбрать). Она отправляет идентификатор выбранного преподавателя в метод
Index
и задает цвет фона.<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
Запустите приложение и выберите вкладку Instructors. На странице отображается Location
(office) из связанной сущности OfficeAssignment
. Если OfficeAssignment имеет значение null, отображается пустая ячейка таблицы.
Щелкните ссылку Select (Выбрать). Изменяется стиль строки.
Добавление курсов, проводимых выбранным преподавателем
Обновите метод OnGetAsync
в файле Pages/Instructors/Index.cshtml.cs
, заменив его следующим кодом:
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
). Выбранный преподаватель извлекается из списка преподавателей в модели представления. Из свойства навигации CourseAssignments
этого преподавателя загружается свойство модели представления Courses
вместе с сущностями 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
.
Метод Single
используется для коллекции, когда она содержит всего один элемент. Метод Single
вызывает исключение, если коллекция пуста или содержит больше одного элемента. Альтернативным вариантом является метод SingleOrDefault
, который возвращает значение по умолчанию (в данном случае null), если коллекция пуста. Использование SingleOrDefault
для пустой коллекции:
- Возникает исключение (из-за попытки найти свойство
Courses
по пустой ссылке). - Сообщение об исключении не так четко указывает на причину проблемы.
Следующий код заполняет свойство Enrollments
модели представления при выборе курса:
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
Добавьте следующую разметку в конец страницы Pages/Instructors/Index.cshtml
Razor.
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@if (Model.Instructor.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table class="table">
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Instructor.Courses)
{
string selectedRow = "";
if (item.CourseID == Model.CourseID)
{
selectedRow = "success";
}
<tr class="@selectedRow">
<td>
<a asp-page="./Index" asp-route-courseID="@item.CourseID">Select</a>
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
Предыдущая разметка отображает список связанных с преподавателем курсов при выборе преподавателя.
Тестирование приложения. Щелкните ссылку Select (Выбрать) на странице преподавателей.
Отображение данных об учащихся
В этом разделе приложение изменяется, чтобы отобразить данные об учащихся для выбранного курса.
Обновите запрос в методе OnGetAsync
в файле Pages/Instructors/Index.cshtml.cs
, заменив его следующим кодом:
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Обновите Pages/Instructors/Index.cshtml
. Добавьте следующую разметку в конец файла:
@if (Model.Instructor.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course
</h3>
<table class="table">
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Instructor.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
Предыдущая разметка отображает список учащихся, которые зачислены на курс.
Обновите страницу и выберите преподавателя. Выберите курс, чтобы увидеть список зачисленных учащихся и их оценки.
Использование метода Single
Метод Single
может передать условие Where
вместо отдельного вызова метода Where
:
public async Task OnGetAsync(int? id, int? courseID)
{
Instructor = new InstructorIndexData();
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
InstructorID = id.Value;
Instructor instructor = Instructor.Instructors.Single(
i => i.ID == id.Value);
Instructor.Courses = instructor.CourseAssignments.Select(
s => s.Course);
}
if (courseID != null)
{
CourseID = courseID.Value;
Instructor.Enrollments = Instructor.Courses.Single(
x => x.CourseID == courseID).Enrollments;
}
}
Приведенный выше подход на основе Single
не дает преимуществ по сравнению с использованием Where
. Некоторые разработчики предпочитают использовать подход Single
.
Явная загрузка
Текущий код указывает упреждающую загрузку для Enrollments
и Students
:
Instructor.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
Предположим, что пользователям редко требуется просматривать зачисления на курс. В этом случае можно оптимизировать работу, загружая данные о зачислении только при их запросе. В этом разделе изменяется OnGetAsync
для использования явной загрузки Enrollments
и Students
.
Измените 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
для выбранного курса. - Сущности
Student
для каждогоEnrollment
.
Обратите внимание, что предыдущий код закомментирует .AsNoTracking()
. Для отслеживаемых сущностей свойства навигации можно загрузить лишь явно.
Тестирование приложения. С точки зрения пользователей приложение работает аналогично предыдущей версии.
Следующее руководство посвящено обновлению связанных данных.
Дополнительные ресурсы
ASP.NET Core