Поделиться через


Руководство. Чтение связанных данных — ASP.NET MVC с помощью EF Core

В предыдущем руководстве мы завершили разработку модели данных School. Из этого руководства вы узнаете, как читать и отображать связанные данные — данные, которые Entity Framework загружает в свойства навигации.

На следующих рисунках изображены страницы, с которыми вы будете работать.

Courses Index page

Instructors Index page

Изучив это руководство, вы:

  • Загрузка связанных данных
  • Создание страницы курсов
  • Создание страницы преподавателей
  • Дополнительные сведения о явной загрузке

Необходимые компоненты

Существует несколько способов, которыми программное обеспечение объектно-реляционного сопоставления (ORM), такое как Entity Framework, может загружать связанные данные в свойства навигации сущности:

  • Безотложная загрузка. При чтении сущности связанные данные извлекаются вместе с ней. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. Настроить Entity Framework Core на использование безотложной загрузки можно при помощи методов Include и ThenInclude.

    Eager loading example

    Вы можете получить часть данных в отдельных запросах, и EF "исправит" свойства навигации. То есть EF автоматически добавляет раздельно извлеченные сущности к соответствующим свойствам навигации ранее извлеченных объектов. Для запроса, получающего связанные данные, можно использовать метод Load вместо метода, который возвращает список или объект, такого как ToList или Single.

    Separate queries example

  • Явная загрузка. При первом чтении сущности связанные данные не извлекаются. Если требуется получение связанных данных, то пишется дополнительный код. Как и в случае безотложной загрузки с отдельными запросами, явная загрузка представляет собой несколько запросов к базе данных. Отличие заключается в том, что при явной загрузке в коде указывается, какие свойства навигации будут загружены. В Entity Framework Core 1.1 для выполнения явной загрузки можно использовать метод Load. Например:

    Explicit loading example

  • Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. Однако при первой попытке доступа к свойству навигации необходимые для этого свойства навигации данные извлекаются автоматически. Запрос к базе данных отправляется каждый раз, когда вы в первый раз пытаетесь получить данные из свойства навигации. Entity Framework Core 1.0 не поддерживает отложенную загрузку.

Замечания, связанные с быстродействием

Если известно, что связанные данные потребуются для каждой полученной сущности, то безотложная загрузка обычно обеспечивает наилучшую производительность, поскольку одиночный запрос к базе данных обычно эффективнее нескольких отдельных запросов для каждой полученной сущности. Пусть, например, на каждом факультете есть десять связанных курсов. Безотложная загрузка всех связанных данных приведет к одиночному запросу (с соединением) и одним циклом приема-передачи данных из базы. Отдельные запросы по курсам для каждого факультета приведут к одиннадцати циклам приема-передачи данных из базы. При высокой задержке дополнительные циклы приема-передачи данных особенно сильно влияют на производительность.

С другой стороны, в некоторых случаях отдельные запросы более эффективны. Безотложная загрузка всех связанных данных в одном запросе может привести к формированию очень сложного соединения, которое SQL сервер не сможет эффективно обработать. Либо, если необходимо получить свойства навигации сущности только для подмножества обрабатываемого набора сущностей, отдельные запросы могут показать большую производительность, поскольку при безотложной загрузке всех данных будет получено больше данных, чем вам необходимо. Если важна производительность, то для выбора наилучшего решения рекомендуется протестировать производительность для обоих случаев.

Создание страницы курсов

Сущность Course включает свойство навигации, которое содержит сущность Department кафедры, к которому привязан курс. Чтобы отобразить в списке курсов название связанной кафедры, необходимо получить свойство Name из сущности Department, находящейся в свойстве навигации Course.Department.

Создайте для типа сущности Course контроллер с именем CoursesController с теми же параметрами шаблона Контроллер MVC с представлениями, использующий Entity Framework, которые мы ранее задали для контроллера StudentsController, как это показано на следующем рисунке:

Add Courses controller

Откройте CoursesController.cs и проверьте метод Index. В автоматически сформированном шаблоне установлена безотложная загрузка свойства навигации Department при помощи метода Include.

Замените метод Index следующим кодом, который использует более подходящее имя для IQueryable, возвращающего сущности Course (courses вместо schoolContext):

public async Task<IActionResult> Index()
{
    var courses = _context.Courses
        .Include(c => c.Department)
        .AsNoTracking();
    return View(await courses.ToListAsync());
}

Откройте Views/Courses/Index.cshtml и замените код шаблона следующим кодом: Изменения выделены:

@model IEnumerable<ContosoUniversity.Models.Course>

@{
    ViewData["Title"] = "Courses";
}

<h2>Courses</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.CourseID)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Credits)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Department)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.CourseID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Credits)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Department.Name)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Мы внесли следующие изменения в код шаблона:

  • Изменен заголовок с Index (Индекс) на Courses (Курсы).

  • Добавлен столбец Number (Номер), отображающий значение свойства CourseID. По умолчанию в шаблоне отсутствуют первичные ключи, поскольку для конечных пользователей они не имеют смысла. Однако в нашем случае первичный ключ имеет смысл, и мы хотим его отобразить.

  • Изменен столбец Department (Кафедра) для отображения названия кафедры. Код отображает свойство Name сущности Department, которая загружена в свойство навигации Department:

    @Html.DisplayFor(modelItem => item.Department.Name)
    

Для просмотра списка с названиями кафедр запустите приложение и выберите вкладку Courses (Курсы).

Courses Index page

Создание страницы преподавателей

В этом разделе мы создадим контроллер и представление для сущности Instructor, чтобы отобразить страницу Instructors:

Instructors Index page

Эта страница считывает и отображает связанные данные следующим образом:

  • Список преподавателей отображает связанные данные сущности OfficeAssignment. Между сущностями Instructor и OfficeAssignment действует связь один к нулю или к одному. Для сущностей OfficeAssignment установлена безотложная загрузка. Как упоминалось ранее, безотложная загрузка обычно эффективнее при получении связанных данных для всех строк главной таблицы. В нашем случае мы хотим отобразить принадлежность к кабинету для каждого преподавателя.

  • Когда пользователь выбирает преподавателя, отображаются связанные сущности Course. Между сущностями Instructor и Course действует связь многие ко многим. Для сущностей Course и связанных сущностей Department используется безотложная загрузка. В этом случае отдельные запросы могут оказаться эффективнее, поскольку нам требуются курсы только для выбранного преподавателя. Этот пример, однако, показывает, как использовать безотложную загрузку для свойств навигации сущностей, которые сами находятся в свойствах навигации.

  • Когда пользователь выбирает курс, отображаются связанные данные из набора сущностей Enrollments. Между сущностями Course и Enrollment действует связь один ко многим. Для сущностей Enrollment и связанных с ними сущностей Student используются отдельные запросы.

Создание модели для представления индекса преподавателей

На странице "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

Создайте контроллер Instructors с действиями чтения/записи Entity Framework, как показано на следующем рисунке:

Add Instructors controller

Откройте InstructorsController.cs и добавьте инструкцию using для пространства имен ViewModels:

using ContosoUniversity.Models.SchoolViewModels;

Замените код Index следующим кодом для выполнения безотложной загрузки связанных данных и размещения их в модели представления.

public async Task<IActionResult> Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Enrollments)
                    .ThenInclude(i => i.Student)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .AsNoTracking()
          .OrderBy(i => i.LastName)
          .ToListAsync();
    
    if (id != null)
    {
        ViewData["InstructorID"] = id.Value;
        Instructor instructor = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single();
        viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        ViewData["CourseID"] = courseID.Value;
        viewModel.Enrollments = viewModel.Courses.Where(
            x => x.CourseID == courseID).Single().Enrollments;
    }

    return View(viewModel);
}

Метод принимает необязательные данные маршрута (id) и строку запроса (courseID), которые содержат значения идентификатора выбранного преподавателя и выбранного курса. Параметры передаются гиперссылками Select на странице.

Код начинается с создания экземпляра модели представления и помещения его в список преподавателей. В коде задается безотложная загрузка для свойств навигации Instructor.OfficeAssignment и Instructor.CourseAssignments. Вместе со свойством CourseAssignments загружается свойство Course, с которым загружаются свойства Enrollments и Department, а с каждой сущностью Enrollment загружается свойство Student.

viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

Так как для представления всегда требуется сущность OfficeAssignment, значительно эффективнее извлекать ее в том же запросе. Получение сущностей Course необходимо при выборе преподавателя на веб-странице, таким образом одиночный запрос окажется предпочтительнее нескольких запросов, только если страница отображается с курсами чаще, чем без них.

В коде повторяются CourseAssignments и Course, так как требуется получить два свойства из Course. Первая строка ThenInclude вызывает получение CourseAssignment.Course, Course.Enrollments и Enrollment.Student.

Дополнительные сведения о том, как включить несколько уровней связанных данных, можно узнать здесь.

viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

В этой точке кода вызов метода ThenInclude получал бы свойства навигации Student, которые нам требуются. Но вызов Include начнет заново получать свойства Instructor, поэтому нам придется еще раз выполнить последовательность команд, указав в этот раз Course.Department вместо Course.Enrollments.

viewModel.Instructors = await _context.Instructors
      .Include(i => i.OfficeAssignment)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Enrollments)
                .ThenInclude(i => i.Student)
      .Include(i => i.CourseAssignments)
        .ThenInclude(i => i.Course)
            .ThenInclude(i => i.Department)
      .AsNoTracking()
      .OrderBy(i => i.LastName)
      .ToListAsync();

Следующий код выполняется при выборе преподавателя. Выбранный преподаватель извлекается из списка преподавателей в модели представления. Затем из свойства навигации CourseAssignments этого преподавателя загружается свойство модели представления Courses вместе с сущностями Course.

if (id != null)
{
    ViewData["InstructorID"] = id.Value;
    Instructor instructor = viewModel.Instructors.Where(
        i => i.ID == id.Value).Single();
    viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}

Метод Where возвращает коллекцию, но с учетом переданных в метод условий в данном случае возвращается только одна сущность Instructor. Метод Single преобразует коллекцию в отдельную сущность Instructor, что позволяет получить доступ к ее свойству CourseAssignments. Свойство CourseAssignments содержит сущности CourseAssignment, из которых нам нужны только связанные сущности Course.

Использовать метод коллекции Single можно, если известно, что коллекция содержит только один элемент. Метод Single вызывает исключение, если передаваемая ему коллекция пустая или содержит больше одного элемента. Альтернативным вариантом является метод SingleOrDefault, который возвращает значение по умолчанию (в данном случае null), если коллекция пуста. Однако в этом случае это все равно приведет к исключению (из-за попытки найти свойство Courses у указателя на null), и из сообщения об исключении нелегко будет понять причину проблемы. При вызове метода Single вы можете также передать условие Where вместо отдельного вызова метода Where:

.Single(i => i.ID == id.Value)

Вместо:

.Where(i => i.ID == id.Value).Single()

Далее, если был выбран курс, то он получается из списка курсов модели представления. Затем из свойства навигации Enrollments этого курса получается свойство модели представления Enrollments вместе с сущностями Enrollment.

if (courseID != null)
{
    ViewData["CourseID"] = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(
        x => x.CourseID == courseID).Single().Enrollments;
}

Отслеживание без отслеживания

Запросы без отслеживания полезны, если результаты используются в сценарии только для чтения. Обычно они быстрее выполняются, так как нет необходимости настраивать сведения об отслеживании изменений. Если сущности, полученные из базы данных, не нужно обновлять, запрос без отслеживания, скорее всего, будет лучше, чем запрос отслеживания.

В некоторых случаях запрос отслеживания эффективнее, чем запрос без отслеживания. Дополнительные сведения см. в разделе "Отслеживание" и "Запросы без отслеживания".

Изменение представления Instructor Index

В Views/Instructors/Index.cshtml замените код шаблона следующим кодом: Изменения выделены.

@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructors)
        {
            string selectedRow = "";
            if (item.ID == (int?)ViewData["InstructorID"])
            {
                selectedRow = "table-success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @foreach (var course in item.CourseAssignments)
                    {
                        @course.Course.CourseID @course.Course.Title <br />
                    }
                </td>
                <td>
                    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
           }
    </tbody>
</table>
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData

@{
    ViewData["Title"] = "Instructors";
}

<h2>Instructors</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
            <th>Hire Date</th>
            <th>Office</th>
            <th>Courses</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Instructors)
        {
            string selectedRow = "";
            if (item.ID == (int?)ViewData["InstructorID"])
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.HireDate)
                </td>
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
                <td>
                    @foreach (var course in item.CourseAssignments)
                    {
                        @course.Course.CourseID @course.Course.Title <br />
                    }
                </td>
                <td>
                    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
                    <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
           }
    </tbody>
</table>

Мы внесли следующие изменения в существующий код:

  • Изменили класс модели на InstructorIndexData.

  • Изменили заголовок страницы с Index на Instructors.

  • Добавили столбец Office, отображающий item.OfficeAssignment.Location только тогда, когда item.OfficeAssignment не равно null. (Так как здесь отношение один к нулю или к одному, то связанной сущности OfficeAssignment может не существовать.)

    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
    
  • Добавили столбец Courses, отображающий курсы, которые ведет конкретный преподаватель. Дополнительные сведения см. в разделе Явный перенос строки статьи по синтаксису Razor.

  • Добавлен код, который по условию добавляет класс CSS Bootstrap к элементу tr выбранного преподавателя. Этот класс задает цвет фона для выделенной строки.

  • В каждой строке непосредственно перед другими ссылками добавили новую гиперссылку с меткой Select, которая отправляет идентификатор выбранного преподавателя в метод Index.

    <a asp-action="Index" asp-route-id="@item.ID">Select</a> |
    

Запустите приложение и перейдите на вкладку "Инструкторы ". На странице отображается свойство Location связанных сущностей OfficeAssignment и пустая ячейка таблицы, если нет связанной сущности OfficeAssignment.

Instructors Index page nothing selected

Views/Instructors/Index.cshtml В файле после закрывающего элемента таблицы (в конце файла) добавьте следующий код. Этот код отображает список связанных с преподавателем курсов, когда преподаватель выбран.


@if (Model.Courses != null)
{
    <h3>Courses Taught by Selected Instructor</h3>
    <table class="table">
        <tr>
            <th></th>
            <th>Number</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == (int?)ViewData["CourseID"])
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">
                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr>
        }

    </table>
}

Этот код считывает свойство Courses модели представления для отображения списка курсов. Он также предоставляет гиперссылку Select, которая отправляет идентификатор выбранного курса в метод действия Index.

Обновите страницу и выберите преподавателя. Вы увидите сетку, которая отображает курсы, назначенные выбранному преподавателю, и для каждого курса отобразится имя связанного факультета.

Instructors Index page instructor selected

После только что добавленного блока кода добавьте следующий код. Он отображает список студентов, которые зачислены на курс при выборе этого курса.

@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Selected Course
    </h3>
    <table class="table">
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        {
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr>
        }
    </table>
}

Этот код считывает свойство Enrollments модели представления для отображения списка студентов, зачисленных на этот курс.

Снова обновите страницу и выберите преподавателя. Затем выберите курс, чтобы увидеть список зачисленных студентов и их оценки.

Instructors Index page instructor and course selected

Сведения о явной загрузке

При получении списка преподавателей в файле InstructorsController.cs мы указали безотложную загрузку для свойства навигации CourseAssignments.

Пусть ожидается, что пользователи лишь изредка будут просматривать список зачисления для выбранных преподавателя и курса. В этом случае, возможно, потребуется загружать данные о зачислении, только когда они запрошены. Чтобы продемонстрировать пример того, как выполнять явную загрузку, замените метод Index следующим кодом, который удаляет безотложную загрузку для Enrollments и загружает это свойство явно. Изменения в коде выделены.

public async Task<IActionResult> Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = await _context.Instructors
          .Include(i => i.OfficeAssignment)
          .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
                .ThenInclude(i => i.Department)
          .OrderBy(i => i.LastName)
          .ToListAsync();

    if (id != null)
    {
        ViewData["InstructorID"] = id.Value;
        Instructor instructor = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single();
        viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
    }

    if (courseID != null)
    {
        ViewData["CourseID"] = courseID.Value;
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
        }
        viewModel.Enrollments = selectedCourse.Enrollments;
    }

    return View(viewModel);
}

Новый код удаляет вызовы метода для регистрации данных из кода, который извлекает ThenInclude сущности инструктора. Также удаляется и AsNoTracking. Если выбраны преподаватели и курс, выделенный код получает сущности Enrollment выбранного курса и сущности Student для каждой сущности Enrollment.

Запустите приложение, перейдите на страницу Instructors Index, и вы не увидите никаких изменений, несмотря на то, что мы изменили способ получения данных.

Получение кода

Скачайте или ознакомьтесь с готовым приложением.

Следующие шаги

Изучив это руководство, вы:

  • Дополнительные сведения о загрузке связанных данных
  • Создание страницы курсов
  • Создание страницы преподавателей
  • Дополнительные сведения о явной загрузке

В следующем учебнике описано, как обновить связанные данные.