Руководство. чтение связанных данных с помощью EF в приложении ASP.NET MVC

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

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

Instructors_index_page_with_instructor_and_course_selected

Скачать завершенный проект

Пример веб-приложения университета Contoso демонстрирует создание приложений ASP.NET MVC 5 с помощью Entity Framework 6 Code First и Visual Studio. Сведения о серии руководств см. в первом руководстве серии.

В этом учебнике рассмотрены следующие задачи.

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

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

Существует несколько способов, с помощью которых Entity Framework может загружать связанные данные в свойства навигации сущности:

  • Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. Однако при первой попытке доступа к свойству навигации необходимые для этого свойства навигации данные извлекаются автоматически. В результате в базу данных отправляются несколько запросов — одна для самой сущности, а другая — каждый раз, когда необходимо извлечь связанные данные для сущности. Класс DbContext включает отложенную загрузку по умолчанию.

    Lazy_loading_example

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

    Eager_loading_example

  • Явная загрузка. Это похоже на отложенную загрузку за исключением того, что вы явно извлечете связанные данные в коде. Это не происходит автоматически при доступе к свойству навигации. Связанные данные загружаются вручную путем получения записи диспетчера состояний объектов для сущности и вызова метода Collection. Load для коллекций или метода Reference. Load для свойств, которые содержат одну сущность. (В следующем примере, если вы хотите загрузить свойство навигации администратора, замените Collection(x => x.Courses) Reference(x => x.Administrator).) Обычно явная загрузка используется только в том случае, если отложенная загрузка отключена.

    Explicit_loading_example

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

Особенности производительности

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

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

Отложенная загрузка может маскировать код, который вызывает проблемы с производительностью. Например, код, который не указывает безотлагательную или явную загрузку, но обрабатывает большой объем сущностей и использует несколько свойств навигации в каждой итерации, может оказаться очень неэффективным (из-за большого количества обращений к базе данных). Приложение, которое хорошо работает при разработке с использованием локального SQL Server, может столкнуться с проблемами производительности при перемещении в базу данных SQL Azure из-за увеличенной задержки и отложенной загрузки. Профилирование запросов к базе данных с реалистичной тестовой нагрузкой поможет определить, подходит ли отложенная загрузка. Дополнительные сведения см. в статье пояснения Entity Framework стратегий: Загрузка связанных данных и использование Entity Framework для сокращения задержки сети до SQL Azure.

Отключить отложенную загрузку перед сериализацией

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

Сериализация также может быть усложнена классами прокси, используемыми Entity Framework, как описано в руководстве по расширенным сценариям.

Одним из способов избежать проблем с сериализацией является сериализация объектов передаваемых данных (DTO) вместо объектов сущностей, как показано в руководстве по использованию веб-API с Entity Framework .

Если вы не используете DTO, можно отключить отложенную загрузку и избежать проблем прокси-сервера, отключив создание прокси-сервера.

Ниже приведены некоторые другие способы отключения отложенной загрузки.

  • Для конкретных свойств навигации опустить ключевое слово virtual при объявлении свойства.

  • Для всех свойств навигации установите для LazyLoadingEnabled значение false, добавьте следующий код в конструктор класса контекста:

    this.Configuration.LazyLoadingEnabled = false;
    

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

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

Создайте контроллер с именем CourseController (не Каурсесконтроллер) для типа сущности Course, используя те же параметры контроллера MVC 5 с представлениями, используя Entity Frameworkный механизм формирования шаблонов, который выполнялся ранее для контроллера Student.

Параметр значения
Класс Model Выберите курс (ContosoUniversity. Models) .
Класс контекста данных Выберите SchoolContext (ContosoUniversity. DAL) .
Имя контроллера Введите каурсеконтроллер. Опять же, не каурсесконтроллер с s. При выборе курса (ContosoUniversity. Models) значение имени контроллера заполняется автоматически. Необходимо изменить значение.

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

Откройте контроллерс\каурсеконтроллер.КС и взгляните на метод Index:

public ActionResult Index()
{
    var courses = db.Courses.Include(c => c.Department);
    return View(courses.ToList());
}

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

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

@model IEnumerable<ContosoUniversity.Models.Course>

@{
    ViewBag.Title = "Courses";
}

<h2>Courses</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.CourseID)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Credits)
        </th>
        <th>
            Department
        </th>
        <th></th>
    </tr>

@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>
            @Html.ActionLink("Edit", "Edit", new { id=item.CourseID }) |
            @Html.ActionLink("Details", "Details", new { id=item.CourseID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.CourseID })
        </td>
    </tr>
}

</table>

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

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

Обратите внимание, что для столбца отдел в шаблонном коде отображается свойство Name сущности Department, которая загружается в свойство навигации Department.

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

Запустите страницу (перейдите на вкладку курсы на домашней странице университета Contoso), чтобы просмотреть список с названиями отделов.

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

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

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

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

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

В папке ViewModels создайте InstructorIndexData.CS и замените имеющийся код следующим кодом:

using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.ViewModels
{
    public class InstructorIndexData
    {
        public IEnumerable<Instructor> Instructors { get; set; }
        public IEnumerable<Course> Courses { get; set; }
        public IEnumerable<Enrollment> Enrollments { get; set; }
    }
}

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

Создание контроллера InstructorController (не Инструкторсконтроллер) с помощью операции чтения и записи EF:

Параметр значения
Класс Model Выберите Instructor (ContosoUniversity. Models) .
Класс контекста данных Выберите SchoolContext (ContosoUniversity. DAL) .
Имя контроллера Введите инструкторконтроллер. Опять же, не инструкторсконтроллер с s. При выборе курса (ContosoUniversity. Models) значение имени контроллера заполняется автоматически. Необходимо изменить значение.

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

Откройте контроллерс\инструкторконтроллер.КС и добавьте оператор using для пространства имен ViewModels:

using ContosoUniversity.ViewModels;

Шаблонный код в методе Index указывает безотлагательную загрузку только для свойства навигации OfficeAssignment:

public ActionResult Index()
{
    var instructors = db.Instructors.Include(i => i.OfficeAssignment);
    return View(instructors.ToList());
}

Замените метод Index следующим кодом, чтобы загрузить дополнительные связанные данные и разместить их в модели представления:

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();
    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }

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

    return View(viewModel);
}

Метод принимает необязательные данные маршрута (id) и параметр строки запроса (courseID), который предоставляет значения ИДЕНТИФИКАТОРов выбранного преподавателя и выбранного курса, а также передает все необходимые данные в представление. Параметры передаются гиперссылками Select на странице.

Код начинается с создания экземпляра модели представления и помещения его в список преподавателей. В коде указывается упреждающая загрузка для Instructor.OfficeAssignment и свойства навигации Instructor.Courses.

var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
    .Include(i => i.OfficeAssignment)
    .Include(i => i.Courses.Select(c => c.Department))
     .OrderBy(i => i.LastName);

Второй метод Include загружает курсы, и для каждого загруженного курса выполняется упреждающая загрузка для свойства навигации Course.Department.

.Include(i => i.Courses.Select(c => c.Department))

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

Если был выбран идентификатор преподавателя, выбранный лектор извлекается из списка инструкторов в модели представления. Затем свойство Courses модели представления загружается с сущностями Course из свойства навигации Coursesного преподавателя.

if (id != null)
{
    ViewBag.InstructorID = id.Value;
    viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}

Метод Where Возвращает коллекцию, но в данном случае условия, передаваемые этому методу, приводят к возврату только одной сущности Instructor. Метод Single Преобразует коллекцию в единую Instructor сущность, которая предоставляет доступ к свойству Courses этой сущности.

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

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

вместо следующего кода:

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

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

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

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

В виевс\инструктор\индекс.кштмлзамените код шаблона следующим кодом. Изменения выделены:

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th></th>
    </tr>

    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.ID == ViewBag.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>
                @Html.ActionLink("Select", "Index", new { id = item.ID }) |
                @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
                @Html.ActionLink("Details", "Details", new { id = item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.ID })
            </td>
        </tr>
    }

    </table>

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

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

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

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

    <td> 
        @if (item.OfficeAssignment != null) 
        { 
            @item.OfficeAssignment.Location  
        } 
    </td>
    
  • Добавлен код, который динамически добавляет class="success" в элемент tr выбранного преподавателя. Это задает цвет фона для выбранной строки с помощью класса начальной загрузки .

    string selectedRow = ""; 
    if (item.InstructorID == ViewBag.InstructorID) 
    { 
        selectedRow = "success"; 
    } 
    <tr class="@selectedRow" valign="top">
    
  • Добавлен новый ActionLink с меткой SELECT непосредственно перед другими ссылками в каждой строке, что приводит к отправке выбранного идентификатора лектора в метод Index.

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

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

@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 == ViewBag.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.

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

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

@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 модели представления, чтобы отобразить список учащихся, зарегистрированных в курсе.

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

Добавление явной загрузки

Откройте InstructorController.CS и посмотрите, как метод Index получает список регистраций для выбранного курса:

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

При получении списка инструкторов вы указали безотлагательную загрузку для свойства навигации Courses и для свойства Department каждого курса. Затем вы помещаете Courses коллекцию в модель представления, и теперь доступ к свойству навигации Enrollments из одной сущности в этой коллекции. Поскольку для свойства навигации Course.Enrollments не указана безотлагательная загрузка, данные из этого свойства отображаются на странице в результате отложенной загрузки.

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

public ActionResult Index(int? id, int? courseID)
{
    var viewModel = new InstructorIndexData();

    viewModel.Instructors = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses.Select(c => c.Department))
        .OrderBy(i => i.LastName);

    if (id != null)
    {
        ViewBag.InstructorID = id.Value;
        viewModel.Courses = viewModel.Instructors.Where(
            i => i.ID == id.Value).Single().Courses;
    }
    
    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        // Lazy loading
        //viewModel.Enrollments = viewModel.Courses.Where(
        //    x => x.CourseID == courseID).Single().Enrollments;
        // Explicit loading
        var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
        db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
        foreach (Enrollment enrollment in selectedCourse.Enrollments)
        {
            db.Entry(enrollment).Reference(x => x.Student).Load();
        }

        viewModel.Enrollments = selectedCourse.Enrollments;
    }

    return View(viewModel);
}

После получения выбранной Course сущности новый код явно загружает свойство навигации Enrollments курса:

db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();

Затем он явным образом загружает сущность Student, связанную с сущностью Enrollment:

db.Entry(enrollment).Reference(x => x.Student).Load();

Обратите внимание, что для загрузки свойства коллекции используется метод Collection, но для свойства, содержащего только одну сущность, используется метод Reference.

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

Получите код

Скачать завершенный проект

Дополнительные ресурсы

Ссылки на другие ресурсы Entity Framework можно найти в ресурсах, рекомендуемых для доступа к данным ASP.NET.

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

В этом учебнике рассмотрены следующие задачи.

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

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