Чтение связанных данных с помощью Entity Framework в приложении MVC ASP.NET (5 из 10)
Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.
Примечание
Если у вас возникла проблема, которую не удается устранить, скачайте завершенную главу и попробуйте воспроизвести проблему. Как правило, решение проблемы можно найти, сравнив код с готовым кодом. Сведения о некоторых распространенных ошибках и способах их устранения см. в статье Ошибки и обходные пути.
В предыдущем руководстве вы завершили модель данных School. В этом руководстве вы прочитаете и отобразите связанные данные, то есть данные, которые Entity Framework загружает в свойства навигации.
На следующих рисунках изображены страницы, с которыми вы будете работать.
Отложенная, неотложная и явная загрузка связанных данных
Платформа Entity Framework может загружать связанные данные в свойства навигации сущности несколькими способами.
Отложенная загрузка. При первом чтении сущности связанные данные не извлекаются. Однако при первой попытке доступа к свойству навигации необходимые для этого свойства навигации данные извлекаются автоматически. Это приводит к отправке нескольких запросов к базе данных — по одному для самой сущности и по одному при каждом получении связанных данных для сущности.
Безотложная загрузка. При чтении сущности связанные данные извлекаются вместе с ней. Обычно такая загрузка представляет собой одиночный запрос с соединением, который получает все необходимые данные. Укажите неотложную загрузку
Include
с помощью метода .Явная загрузка. Это похоже на отложенную загрузку, за исключением того, что вы явным образом извлекаете связанные данные в коде; Это не происходит автоматически при доступе к свойству навигации. Связанные данные загружают вручную, получая запись диспетчера состояний объектов для сущности и вызывая
Collection.Load
метод для коллекций илиReference.Load
метод для свойств, которые содержат одну сущность. (В следующем примере, если вы хотите загрузить свойство навигации "Администратор", заменитеCollection(x => x.Courses)
Reference(x => x.Administrator)
на .)
Так как они не получают значения свойств сразу, отложенная загрузка и явная загрузка также называются отложенной загрузкой.
Как правило, если вы знаете, что вам нужны связанные данные для каждой полученной сущности, то безотложная загрузка обеспечивает наилучшую производительность, так как один запрос, отправленный в базу данных, обычно более эффективен, чем отдельные запросы для каждой полученной сущности. Например, в приведенных выше примерах предположим, что каждый отдел имеет десять связанных курсов. Пример с неотложной загрузкой приведет только к одному запросу (присоединению) и одному циклу к базе данных. Примеры отложенной загрузки и явной загрузки приводят к одиннадцати запросам и одиннадцати круговых путей к базе данных. При высокой задержке дополнительные циклы приема-передачи данных особенно сильно влияют на производительность.
С другой стороны, в некоторых сценариях отложенная загрузка является более эффективной. Неотложная загрузка может привести к созданию очень сложного соединения, которое SQL Server не может эффективно обрабатываться. Или если требуется доступ к свойствам навигации сущности только для подмножества обрабатываемых вами наборов сущностей, отложенная загрузка может работать лучше, так как при неотложной загрузке будет получено больше данных, чем требуется. Если важна производительность, то для выбора наилучшего решения рекомендуется протестировать производительность для обоих случаев.
Как правило, явная загрузка используется только в том случае, если отложенная загрузка отключена. Один из сценариев, когда следует отключить отложенную загрузку, — это во время сериализации. Отложенная загрузка и сериализация не очень хорошо сочетаются, и если вы не будете осторожны, вы можете в конечном итоге запросить значительно больше данных, чем предполагалось, если включена отложенная загрузка. Сериализация обычно выполняется путем доступа к каждому свойству экземпляра типа. Доступ к свойствам запускает отложенную загрузку, и эти отложенные загруженные сущности сериализуются. Затем процесс сериализации обращается к каждому свойству отложенных сущностей, что может привести к еще более отложенной загрузке и сериализации. Чтобы предотвратить эту цепную реакцию, отключите отложенную загрузку перед сериализизовывом сущности.
Класс контекста базы данных выполняет отложенную загрузку по умолчанию. Отключить отложенную загрузку можно двумя способами:
Для определенных свойств навигации
virtual
опустите ключевое слово при объявлении свойства.Для всех свойств навигации задайте значение
LazyLoadingEnabled
false
. Например, можно поместить следующий код в конструктор класса контекста:this.Configuration.LazyLoadingEnabled = false;
Отложенная загрузка может маскирует код, который вызывает проблемы с производительностью. Например, код, который не задает неотложную или явную загрузку, но обрабатывает большой объем сущностей и использует несколько свойств навигации в каждой итерации, может быть очень неэффективным (из-за большого количества циклов передачи к базе данных). Приложение, которое хорошо работает в разработке с помощью локального сервера SQL Server, может иметь проблемы с производительностью при перемещении в базу данных Azure SQL из-за увеличения задержки и отложенной загрузки. Профилирование запросов базы данных с реалистичной тестовой нагрузкой поможет определить, подходит ли отложенная загрузка. Дополнительные сведения см. в разделах Demystifying Entity Framework Strategies: Loading Related Data и Using the Entity Framework to Reduce Network Latency to SQL Azure.
Создание страницы индекса курсов, на котором отображается название отдела
Сущность Course
включает свойство навигации, которое содержит сущность Department
кафедры, к которому привязан курс. Чтобы отобразить имя назначенного отдела в списке курсов, необходимо получить Name
свойство из сущности Department
, которая находится в свойстве навигации Course.Department
.
Создайте контроллер с именем CourseController
для Course
типа сущности, используя те же параметры, что и для контроллера, как показано на Student
следующем рисунке (за исключением того, что в отличие от изображения, класс контекста находится в пространстве имен DAL, а не в пространстве имен Models):
Откройте Файл Controllers\CourseController.cs и просмотрите Index
метод :
public ViewResult 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>
<tr>
<th></th>
<th>Number</th>
<th>Title</th>
<th>Credits</th>
<th>Department</th>
</tr>
@foreach (var item in Model) {
<tr>
<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>
<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>
</tr>
}
</table>
Мы внесли следующие изменения в код шаблона:
- Изменен заголовок с Index (Индекс) на Courses (Курсы).
- Перемещены ссылки на строки влево.
- Добавлен столбец под заголовком Число , отображающий
CourseID
значение свойства. (По умолчанию первичные ключи не являются шаблонами, так как обычно они не имеют смысла для конечных пользователей. Однако в этом случае первичный ключ имеет смысл, и вы хотите показать его.) - Изменен заголовок последнего столбца с DepartmentID (имя внешнего ключа на
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; }
}
}
Добавление стиля для выбранных строк
Чтобы пометить выделенные строки, требуется другой цвет фона. Чтобы предоставить стиль для этого пользовательского интерфейса, добавьте следующий выделенный код в раздел /* info and errors */
Content\Site.css, как показано ниже:
/* info and errors */
.selectedrow
{
background-color: #a4d4e6;
}
.message-info {
border: 1px solid;
clear: both;
padding: 10px 20px;
}
Создание контроллера и представлений преподавателя
Создайте InstructorController
контроллер, как показано на следующем рисунке:
Откройте файл Controllers\InstructorController.cs и добавьте инструкцию using
ViewModels
для пространства имен:
using ContosoUniversity.ViewModels;
Шаблонный код в методе Index
задает неотложную загрузку только для свойства навигации OfficeAssignment
:
public ViewResult 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.InstructorID == 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 на странице.
Совет
Данные маршрута
Данные маршрута — это данные, которые связыватель модели обнаружил в сегменте URL-адреса, указанном в таблице маршрутизации. Например, маршрут по умолчанию задает сегменты controller
, action
и id
:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
В следующем URL-адресе маршрут по умолчанию сопоставляется Instructor
как controller
, как action
и Index
1 как id
; это значения данных маршрута.
http://localhost:1230/Instructor/Index/1?courseID=2021
"?courseID=2021" — это строковое значение запроса. Связыватель модели также будет работать, если передать в id
качестве значения строки запроса:
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
URL-адреса создаются операторами ActionLink
в представлении Razor. В следующем коде id
параметр соответствует маршруту по умолчанию, поэтому id
добавляется в данные маршрута.
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
В следующем коде courseID
не соответствует параметру в маршруте по умолчанию, поэтому он добавляется в качестве строки запроса.
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
Код начинается с создания экземпляра модели представления и помещения его в список преподавателей. Код задает неотложную загрузку для свойства навигации 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.InstructorID == id.Value).Single().Courses;
}
Метод Where
возвращает коллекцию, но в этом случае условия, передаваемые этому методу, приводят к возврату только одной Instructor
сущности. Метод Single
преобразует коллекцию в отдельную сущность Instructor
, что позволяет получить доступ к ее свойству Courses
.
Метод Single используется в коллекции, если известно, что в коллекции будет только один элемент. Метод Single
создает исключение, если переданная ему коллекция пуста или имеет несколько элементов. Альтернативой является SingleOrDefault, которая возвращает значение по умолчанию (null
в данном случае), если коллекция пуста. Однако в этом случае это по-прежнему приведет к возникновению исключения (при попытке найти Courses
свойство в null
ссылке), а сообщение об исключении будет менее четко указывать причину проблемы. При вызове Single
метода можно также передать Where
условие, а не вызывать Where
метод отдельно:
.Single(i => i.InstructorID == id.Value)
вместо следующего кода:
.Where(I => i.InstructorID == 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>
<tr>
<th></th>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
</tr>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.InstructorID == ViewBag.InstructorID)
{
selectedRow = "selectedrow";
}
<tr class="@selectedRow" valign="top">
<td>
@Html.ActionLink("Select", "Index", new { id = item.InstructorID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) |
@Html.ActionLink("Details", "Details", new { id = item.InstructorID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.InstructorID })
</td>
<td>
@item.LastName
</td>
<td>
@item.FirstMidName
</td>
<td>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
</tr>
}
</table>
Мы внесли следующие изменения в существующий код:
Изменили класс модели на
InstructorIndexData
.Изменили заголовок страницы с Index на Instructors.
Перемещены столбцы ссылок на строки влево.
Удален столбец FullName .
Добавлен столбец Office , который отображается
item.OfficeAssignment.Location
только в том случае, еслиitem.OfficeAssignment
значение не равно NULL. (Так как это отношение "один к нулю" или "один", может не быть связаннойOfficeAssignment
сущности.)<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
Добавлен код, который будет динамически добавляться
class="selectedrow"
вtr
элемент выбранного преподавателя. При этом задается цвет фона для выбранной строки с помощью созданного ранее класса CSS. (Атрибутvalign
будет полезен в следующем руководстве при добавлении многострочного столбца в таблицу.)string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "selectedrow"; } <tr class="@selectedRow" valign="top">
Добавлена новая
ActionLink
метка Select непосредственно перед другими ссылками в каждой строке, что приводит к отправке выбранного идентификатора преподавателя вIndex
метод.
Запустите приложение и выберите вкладку Преподаватели. Если связанной OfficeAssignment
сущности нет, Location
на странице отображается свойство связанных OfficeAssignment
сущностей и пустая ячейка таблицы.
В файле Views\Instructor\Index.cshtml после закрывающего table
элемента (в конце файла) добавьте следующий выделенный код. Отобразится список курсов, связанных с инструктором при выборе преподавателя.
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
</tr>
}
</table>
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table>
<tr>
<th></th>
<th>ID</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Courses)
{
string selectedRow = "";
if (item.CourseID == ViewBag.CourseID)
{
selectedRow = "selectedrow";
}
<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
метод действия.
Примечание
CSS-файл кэшируется браузерами. Если при запуске приложения изменения не отображаются, выполните жесткое обновление (удерживайте нажатой клавишу CTRL при нажатии кнопки Обновить или нажмите клавиши CTRL+F5).
Запустите страницу и выберите преподавателя. Вы увидите сетку, которая отображает курсы, назначенные выбранному преподавателю, и для каждого курса отобразится имя связанного факультета.
После только что добавленного блока кода добавьте следующий код. Он отображает список студентов, которые зачислены на курс при выборе этого курса.
@if (Model.Enrollments != null)
{
<h3>
Students Enrolled in Selected Course</h3>
<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.InstructorID == id.Value).Single().Courses;
}
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
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 карте содержимого доступа к данным.