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


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

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

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

Снимок экрана: страница

Instructors_index_page_with_instructor_and_course_selected

Скачивание завершенного проекта

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

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

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

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

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

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

    Lazy_loading_example

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

    Eager_loading_example

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

    Explicit_loading_example

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

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

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

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

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

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

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

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

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

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

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

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

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

    this.Configuration.LazyLoadingEnabled = false;
    

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

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

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

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

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

Откройте controllers\CourseController.cs и просмотрите Index метод:

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

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

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

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

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

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

Обратите внимание, что для столбца 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; }
    }
}

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

Создайте контроллер (не InstructorsController) с действием InstructorController чтения и записи EF:

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

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

Откройте controllers\InstructorController.cs и добавьте инструкцию 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);
}

Метод принимает необязательные данные маршрута () и параметр строки запроса (idcourseID), который предоставляет значения идентификаторов выбранного инструктора и выбранного курса, а также передает все необходимые данные в представление. Параметры передаются гиперссылками 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 этого преподавателя загружается свойство модели представления Courses вместе с сущностями Course.

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

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

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

@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 элемент выбранного инструктора. Этот параметр задает цвет фона для выделенных строк c помощью класса Bootstrap.

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

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

В файле Views\Instructor\Index.cshtml после закрывающего 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 доступ к данным — рекомендуемые ресурсы.

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

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

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

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