Загрузка данных с Entity Framework в приложении ASP.NET MVC

Это очередная статья из серии статей:

В прошлом уроке мы завершили работу над моделью данных School. В этом уроке мы загрузим и отобразим необходимые данные, подгружаемые Entity Framework в navigation properties.

На иллюстрации изображены результаты нашей.

clip_image001

clip_image002

Lazy, Eager, и Explicit загрузка необходимых данных

Есть несколько методов, которыми пользуется EF для загрузки необходимых данных в navigation properties сущности:

  • Lazyloading. При первом обращении к сущности, соответствующие связанные данные не загружаются. Однако, при первом обращении к navigation property, связанные данные загружаются автоматически. При этом к базе совершается множество запросов: один для сущности и по одному каждый раз при загрузке данных.

clip_image003

  • Eagerloading. Данные загружаются при обращении к сущности. Обычно это сопровождается одним запросом join, который возвращает все данные. Можно указать использование eager loading с помощью метода Include.

clip_image004

  • Explicitloading. Данный метод похож на lazy loading за исключением того, что вы сами указываете на загрузку данных – это не происходит автоматически при обращении navigation property. Данные загружаются вручную с помощью object state manager сущности и вызова метода Collection.Load для коллекций и метода Reference.Load для свойств с одним значением. (в примере, если вы хотите загрузить Administrator navigation property, замените Collection(x => x.Courses) на Reference(x => x.Administrator).)

clip_image005

Из-за того, что данные не сразу же загружаются, lazy loading и explicit loading имеют общее название deferredloading.

В целом, если у вас есть необходимость в данных для каждой сущности, метод eager loading предлагает наилучшую производиетльность потому, что один запрос обычно эффективнее нежели множество запросов для каждой сущности. Для примера, представьте, что на каждом факультете проводится десять курсов. Пример для eager loading будет сопровождаться одним join-запросом. Примеры для lazy loading и explicit loading будут сопровождаться одиннадцатью.

С другой стороны, если доступ к navigation properties сущностям осуществляется редко или используется малое количество сущностей, lazy loading может быть эффективнее - eager loading будет загружать больше данных чем нужно. Обычно explicit loading используется только тогда, когда выключен lazy loading. Допустим, есть ситуация, когда lazy loading может быть выключен в процессе сериализации, когда вы уверены, что все navigation properties вам не нужны. Если lazy loading включен, все navigation properties загрузятся автоматически, так как сериализация обращается ко всем свойствам.

Класс контекста базы данных использует lazy loading по умолчанию. Есть два способа выключить lazy loading:

  • При объявлении navigation properties пропустите указание модификатора virtual.
  • Для всех navigation properties укажите LazyLoadingEnabled как false.

Lazy loading может скрывать код, вызывающий проблемы с производительностью. Например, код, не использующий eager или explicit loading но использующий большое количество сущностей и несколько navigation properties в цикле может быть очень неэффективным из-за большого количества обращений к базе данных, но всё будет в порядке, если использовуется lazy loading. Временное отключение lazy loading – один из способов отыскать код, ориентирующийся на использование lazy loading. В этом navigation properties будут равны Null и будет инициирована ошибка.

Создание страницы CoursesIndex

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

Создайте контроллер типа Course:

clip_image006

В Controllers\CourseController.cs обратите внимание на метод Index:

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

Автоматический scaffolding определяет использование eager loading для Department navigation property с помощью метода 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>

Были внесены следующие изменения:

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

Обратите внимание, что для последнего столбца отображено значение свойства Name сущности Department из Department navigation property:

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

Выберите пункт Courses чтобы увидеть список с названиями факультетов.

clip_image001[1]

Создание InstructorsIndexPageсо списком курсов и слушателей этих курсов

Вы создадите контроллер и представление для сущности Instructor для отображения страницы Instructors Index:

clip_image002[1]

Загрузка и отображение данных происходит по следующему сценарию:

  • Список преподавателей отображает соответствующие данные сущности OfficeAssignment. Сущности Instructor и OfficeAssignment связаны один-к-нулю-или-к-одному. Для сущности OfficeAssignment используется eager loading.
  • Когда пользователь выбирает преподавателя, отображаются связанные сущности Course. Сущности Instructor и Course связаны многие-ко-многим. Вы будете использовать eager loading для сущностей Course и связанным с ними сущностей Department. В этом случае lazy loading будет более эффективно, потому что необходимо выгружать данные о курсах только для выбранного преподавателя. Однако в примере показано использование eager loading.
  • Когдап ользователь выбирает курс, отображаются данные сущности Enrollments. Сущности Course и Enrollment связаны один-ко-многим. Вы добавите explicit loading для сущности Enrollment и связанным с ней сущностей Student. (При включенном lazy loading использование Explicit loading необязательно, но мы просто покажем как работает explicit loading.)

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

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

В папке ViewModels создайте InstructorIndexData.cs:

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

Стили для выделенных столбцов

Для выделенных столбцов необходим отдельный цвет фона. Для того, чтобы сделать это, добавьте следующий код в Content \ Site . css:

 /* MISC   
----------------------------------------------------------*/ 
.selectedrow  
{  
    background-color: #EEEEEE;  
}

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

Создайте контроллер типа Instructor:

clip_image007

В Controllers\InstructorController.cs добавьте using для ViewModels:

using ContosoUniversity.ViewModels;

Сгенерированный код в методе Index определяет использование eager loading только для OfficeAssignment navigation property:

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

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

 public ActionResult Index(Int32? id, Int32? 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 значений выбранных преподавателя и курса, и затем передаёт необходимые данные в представление. ID поступают от ссылок Select на странице.

Код начинается созданием экземпляра модели представления и передачи в него списка преподавателей:

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

Мы определяем eager loading для Instructor.OfficeAssignment и Instructor.Courses navigation property. Для связанных сущностей Course eager loading определяется для Course.Department navigation property с использованием метода Select в методе Include. Результаты сортируются по фамилии.

Если выбран преподаватель, то данный преподаватель извлекается из списка преподавателей в модели данных, после чего свойство Courses инициализируется сущностями Course из соответствующей преподавателю Courses navigation property.

 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 используется на коллекции если известно, что коллекция будет состоять из одного элемента. Данный метод выбрасывает исключения, если коллекция пустая или состоит из более чем одного элемента. Однако и в данном случае будет выброшено исключение (из-за свойства Course с ссылкой null). При вызове Single вместо отдельного вызова Where можно передать само условие:

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

Вместо:

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

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

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

И, наконец, возвращение в модель:

return View(viewModel);

Редактированиепредставления Instructor Index

В 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>  
                @String.Format("{0:d}", item.HireDate)  
            </td>  
            <td>  
                @if (item.OfficeAssignment != null)  
                {  
                    @item.OfficeAssignment.Location   
                }  
            </td>  
        </tr>  
    }  
</table>

Изменения:

  • Заголовок страницы изменён на Instructors.
  • Moved the row link columns to the left.
  • Убран столбец 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, что позволяет передать ID выбранного преподавателя в метод Index.

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

clip_image008

в Views \ Instructors \ Index . cshtml после контейнера 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 передаётся ID выбранного курса.

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

clip_image009

Note если выбранная строка не меняет цвет, обновите страницу, иногда это нужно для загрузки файла .css.

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

 @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>  
                    @item.Grade  
                </td>  
            </tr>  
        }  
    </table>  
}

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

Выберите преподавателя и щёлкните на курсе, чтобы увидеть учащихся студентов и их оценки.

clip_image002[2]

Добавление ExplicitLoading

В InstructorController . cs и обратите внимание на то, как метод Index загружает список учащихся на выбранном курсе:

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

Во время загрузки списка преподавателей вы определили eager loading для Courses navigation property и свойства каждого из курсов Department, после чего отправили коллекцию Courses в модель представления, и теперь загружаете Enrollments navigation property из одной из сущностей в этой коллекции. Из-за того, что вы не определили eager loading для Course.Enrollments navigation property, данные этого свйоства появляются на странице в результате lazy loading.

Если отключить lazy loading, свойство Enrollments будет равно null независимо от того, сколько учащихся учится на этом курсе. В этом случае, чтобы инициализировать свойство Enrollments, вы должны определить для него eager loading или explicit loading. Для определения explicit loading замените код метода Index на:

 public ActionResult Index(Int32? id, Int32? 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 = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments; 
    } 
 
    return View(viewModel); 
}

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

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

Затем явно загружаются сущности Student:

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

Обратите вниамние на то, что вы используете метод Collection для инициализации свойства коллекции, но для свойства с одним элементом вы используете метод Reference. Теперь можно открыть страницу Instructor Index – ничего не изменилось внешне, но изменился принцип загрузки данных.

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

--

Это перевод оригинальной статьи Reading Related Data with the Entity Framework in an ASP.NET MVC Application. Благодарим за помощь в переводе Александра Белоцерковского.