Руководство. Чтение связанных данных с помощью EF в ASP.NET приложении MVC
В предыдущем руководстве вы завершили работу с моделью данных School. В этом руководстве вы прочитаете и отобразите связанные данные, то есть данные, которые Платформа Entity Framework загружает в свойства навигации.
На следующих рисунках изображены страницы, с которыми вы будете работать.
Скачивание завершенного проекта
Пример веб-приложения Университета Contoso демонстрирует создание ASP.NET приложений MVC 5 с помощью Entity Framework 6 Code First и Visual Studio. Сведения о серии руководств см. в первом руководстве серии.
В этом учебнике рассмотрены следующие задачи.
- Загрузка связанных данных
- Создание страницы курсов
- Создание страницы преподавателей
Предварительные требования
Загрузка связанных данных
Платформа Entity Framework может загружать связанные данные в свойства навигации сущности несколькими способами.
Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. Однако при первой попытке доступа к свойству навигации необходимые для этого свойства навигации данные извлекаются автоматически. Это приводит к отправке нескольких запросов к базе данных — по одному для самой сущности и по одному при каждом извлечении связанных данных для сущности. Класс
DbContext
включает отложенную загрузку по умолчанию.Безотложная загрузка. При чтении сущности связанные данные извлекаются вместе с ней. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. Укажите неотложную загрузку
Include
с помощью метода .Явная загрузка. Это похоже на отложенную загрузку, за исключением того, что вы явным образом извлекаете связанные данные в коде; Это не происходит автоматически при доступе к свойству навигации. Вы загружаете связанные данные вручную, получая запись диспетчера состояний объектов для сущности и вызывая метод Collection.Load для коллекций или метод Reference.Load для свойств, которые содержат одну сущность. (В следующем примере, если вы хотите загрузить свойство навигации "Администратор", замените
Collection(x => x.Courses)
Reference(x => x.Administrator)
на .) Как правило, вы используете явную загрузку только в том случае, если отложенная загрузка отключена.
Так как значения свойств не извлекаются сразу, отложенная загрузка и явная загрузка также называются отложенной загрузкой.
Особенности производительности
Если известно, что связанные данные потребуются для каждой полученной сущности, то безотложная загрузка обычно обеспечивает наилучшую производительность, поскольку одиночный запрос к базе данных обычно эффективнее нескольких отдельных запросов для каждой полученной сущности. Например, в приведенных выше примерах предположим, что каждый отдел имеет десять связанных курсов. Пример с неотложной загрузкой приведет только к одному запросу (соединению) и один круговой путь к базе данных. Примеры отложенной загрузки и явной загрузки приводят к одиннадцати запросам и одиннадцати круговых путей к базе данных. При высокой задержке дополнительные циклы приема-передачи данных особенно сильно влияют на производительность.
С другой стороны, в некоторых сценариях отложенная загрузка является более эффективной. Неотложная загрузка может привести к созданию очень сложного соединения, которое SQL Server не может эффективно обрабатываться. Или если вам нужно получить доступ к свойствам навигации сущности только для подмножества набора обрабатываемых сущностей, отложенная загрузка может работать лучше, так как при неотложной загрузке будет получено больше данных, чем требуется. Если важна производительность, то для выбора наилучшего решения рекомендуется протестировать производительность для обоих случаев.
Отложенная загрузка может маскировать код, который вызывает проблемы с производительностью. Например, код, который не указывает неотложную или явную загрузку, но обрабатывает большой объем сущностей и использует несколько свойств навигации в каждой итерации, может быть очень неэффективным (из-за большого количества кругового пути к базе данных). Приложение, которое хорошо работает в разработке с использованием локального сервера SQL Server, может иметь проблемы с производительностью при перемещении в базу данных Azure SQL из-за повышенной задержки и отложенной загрузки. Профилирование запросов базы данных с реалистичной тестовой нагрузкой поможет определить, подходит ли отложенная загрузка. Дополнительные сведения см. в разделах Demystifying Entity Framework Strategies: Loading Related Data и Using the Entity Framework to Reduce Network Latency to SQL Azure.
Отключение отложенной загрузки перед сериализацией
Если вы оставить отложенную загрузку включенной во время сериализации, вы можете в конечном итоге запрашивать значительно больше данных, чем вы планировали. Сериализация обычно выполняется путем доступа к каждому свойству экземпляра типа. Доступ к свойствам активирует отложенную загрузку, и эти отложенные загруженные сущности сериализуются. Затем процесс сериализации обращается к каждому свойству сущностей с отложенной загрузкой, что может привести к еще более отложенной загрузке и сериализации. Чтобы предотвратить эту цепную реакцию, отключите отложенную загрузку перед сериализизовывом сущности.
Сериализацию также могут усложнять классы-посредники, которые используются в Entity Framework, как описано в руководстве по расширенным сценариям.
Одним из способов избежать проблем сериализации является сериализация объектов передачи данных (DTO) вместо объектов сущностей, как показано в руководстве Использование веб-API с Entity Framework .
Если вы не используете DTO, можно отключить отложенную загрузку и избежать проблем с прокси-сервером, отключив создание прокси-сервера.
Ниже приведены некоторые другие способы отключения отложенной загрузки.
Для определенных свойств навигации
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. При выборе параметра Course (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
. По умолчанию первичные ключи не создаются, так как обычно они не имеют смысла для конечных пользователей. Однако в нашем случае первичный ключ имеет смысл, и мы хотим его отобразить. - Переместил столбец Department в правую сторону и изменил его заголовок. Шаблон правильно выбрал отображение
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; }
}
}
Создание контроллера и представлений преподавателя
InstructorController
Создайте контроллер (не InstructorsController) с действием чтения и записи EF:
Параметр | Значение |
---|---|
Класс модели | Выберите Instructor (ContosoUniversity.Models) (Преподаватель (ContosoUniversity.Models)). |
Класс контекста данных | Выберите SchoolContext (ContosoUniversity.DAL). |
Имя контроллера | Введите InstructorController. Опять же, не InstructorsController с 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);
}
Метод принимает необязательные данные маршрута (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
метод загружает Courses и для каждого загружаемого курса выполняет неотложную загрузку для свойства навигации 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 используется в коллекции, если известно, что в коллекции будет только один элемент. Метод 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
элемент выбранного преподавателя. При этом задается цвет фона для выбранной строки с помощью класса Bootstrap .string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "success"; } <tr class="@selectedRow" valign="top">
Добавлена новая
ActionLink
метка Select непосредственно перед другими ссылками в каждой строке, что приводит к отправке выбранного идентификатора преподавателя вIndex
метод.
Запустите приложение и выберите вкладку Преподаватели. Если связанной OfficeAssignment
сущности нет, Location
на странице отображается свойство связанных 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 доступ к данным — рекомендуемые ресурсы.
Дальнейшие действия
В этом учебнике рассмотрены следующие задачи.
- Дополнительные сведения о загрузке связанных данных
- Создание страницы курсов
- Создание страницы преподавателей
В следующем руководстве описано, как обновить связанные данные.