为ASP.NET MVC应用程序读取相关数据

这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第七篇:为ASP.NET MVC应用程序读取相关数据

原文: Reading Related Data with the Entity Framework in an ASP.NET MVC Application

在之前的教程中您已经完成了学校数据模型。在本教程中你将学习如何读取和现实相关的数据——这里指的是通过实体框架的导航属性加载的数据。

下方的截图显示了你将要完成的页面。

延迟、预先和现实加载相关数据

实体框架拥有多种将相关数据从一个实体的导航属性中进行加载的方法:

  • 延迟加载(Lazy Loading)。当实体第一次被读取时,相关数据不会被获取。但是,当你第一次尝试存取导航属性时,该导航属性所需的数据会自动加载。结果会使用多个查询发送到数据库——一次是读取实体本身,然后是每个相关的实体。DbContext类默认是使用延迟加载的。

  • 预先加载(Eager Loading)。当实体读取时立即获取与该实体相关的数据。这通常会导致在单个连接查询中检索出所有所需要的数据。您可以通过使用Include方法来指定预先加载。

  • 显式加载(Explicit Loading)。有点类似于延迟加载,只是你在代码中显式地获取相关数据。当您访问一个导航属性时,它不会自动加载。你需要通过使用实体的对象状态管理器并调用集合上的Collection.Load方法或通过持有单个实体的属性的Reference.Load方法来手动加载相关数据。(在下面的示例中,如果你想要加载管理员导航属性,你需要使用Reference(x => x.Administrator)来替换Collection(x => x.Courses))

因为延迟加载和显式加载都不立即检索属性的值,所以它们也被称为推迟加载。

性能注意事项

如果你知道你立即需要每个实体的相关数据,预先加载通常提供最佳的性能。因为单个查询发送到数据库并一次性获取数据的效率通常比在每个实体上再发出一次查询的效率更高。例如,在上面的示例中,假定每个系有十个相关的课程,预先加载会导致只有一个查询(join联合查询)往返于数据库。延迟加载和显式加载两者都将造成11个查询和往返。在高延迟的情况下,额外的查询和往返通常是不利的。

另一方面,在某些情况下使用延迟加载的效率更高。预先加载可能会导致生成SQL Server不能有效处理的非常复杂的联接查询。或者,如果您正在处理的是需要访问的某个实体的导航属性,该属性仅为实体集的一个子集,延迟加载可能比预先加载性能更好,因为预先加载会将所有的数据全部加载,即使你不需要访问它们。如果应用程序的性能是极为重要的,你最好测试并在这两种方法之间选择一种最佳的。

延迟加载可能会屏蔽一些导致性能问题的代码。例如,代码没有指定预先或显式加载但在处理大量实体并时在每次迭代中都使用了导航属性的情况下,代码的效率可能会很低(因为会有大量的数据库往返查询)。一个在开发环境下表现良好的应用程序可能会在移动到Windows Azure SQL数据库时由于增加了延迟导致延迟加载的性能下降。你应当分析并测试以确保延迟加载是否是适当的。详细信息,请参阅 Demystifying Entity Framework Strategies: Loading Related Data 和 Using the Entity Framework to Reduce Network Latency to SQL Azure 。

在序列化之前禁用延迟加载

如果你在序列化期间启用了延迟加载,最终你可能会查询到比预期更多的数据。序列化一般会访问类的每个属性。而属性访问触发延迟加载,然后会将延迟加载的实体也进行序列化。最终有可能会导致更多的延迟加载及属性的序列化,要防止这种链式反应,请在实体序列化之前禁用延迟加载。

有一种避免序列化问题的方式是序列化数据传输对象(DTO)而不是实体对象,如 Using Web API with Entity Framework 教程所示。

如果您不想使用DTO,您可以禁用延迟加载并避免通过 disabling proxy creation 来避免代理问题。

这里 有一些禁用延迟加载的方式:

  • 对于特定的导航属性,省略virtual关键字声明。

  • 对于所有的导航属性,可以设置LazyLoadingEnabled为false,将下面的代码放在您上下文类的构造函数中:this.Configuration.LazyLoadingEnabled = false;

创建课程页面,显示系名称

Course实体包含一个导航属性,里面包括了分配给该课程的Department实体。若要在课程列表中显示已分配系的名称,你需要从Department实体中获取Name属性,即Course.Department导航属性。

为Course实体类型新建一个“包含视图的MVC 5控制器(使用Entity Framework)”控制器并命名为CourseController,使用在之前你创建Student控制器一样的设置,如下图所示:

打开该控制器并查看Index方法:

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

你看到自动生成的脚手架使用Include方法来指定了Department属性的预先加载。

打开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改为课程。

  • 添加了一个数字行用来显示CourseID属性值。缺省情况下,脚手架不会生成主键模板,因为通常他们对最终用户是没有意义的。但在本例中,我们只是用来展示你可以这样将其显示出来而已。

  • 将课程行移动到右侧并修改了它的标题,脚手架正确的选择了Department实体的Name属性,但在本课程页面中,列标题应当是系,而不是Name。

请注意在系行中,脚手架代码显示系实体的Name属性使通过导航属性来加载的。

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

运行该页面(选择课程选项卡)以查看系名称列表。

创建讲师页面以显示课程及注册信息

在这一节中您将创建一个控制器和使用讲师实体的视图来显示讲师页面。

此页面通过以下方式来读取和现实相关数据:

  • 讲师列表显示OfficeAssignment实体的相关数据。Instructor和OfficeAssignment实体之间是一对一或零的关系,您可以使用OfficeAssignment实体额预先加载。如前所述,当你需要主表的所有关联数据时,预先加载是更有效的。在这里,您想要显示所有讲师的办公室分配情况。

  • 当用户选择一名讲师时将显示相关的Course实体。Instructor和Course实体之间存在多对多的关系。您也可以在Course实体和它们相关的Department实体上使用预先加载。但在这里,延迟加载可能更有效,因为您仅需要已选择讲师的课程信息。实际上,这里演示了如何使用延迟加载来加载导航属性之中的导航属性。

  • 当用户选择一门课程时,注册实体记中相关的数据被显示。Course和Enrollment实体是一对多的关系。您将添加显式加载到Enrollment实体及它们相关的Student实体。(显式加载其实是不必要的,但这里只是演示了如何执行显式加载)

为讲师索引视图创建ViewModel

讲师页面显示了三个不同的表格,所以您将创建一个包含三个属性的视图模型,每个属性持有一个表格所需的数据。

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

创建讲师控制器和视图

和之前CourseController控制器一样,创建一个InstructorController控制器,如下图所示:

打开Controller\InstructorController.cs并添加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)用来提供所选讲师及课程的ID值并将所有需要的数据传给视图。页面上的选择超链接将提供这些参数。

代码首先创建视图模型的实例并将讲师列表放进模型中,该代码指定在OfficeAssignment和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方法加载课程,并且为每个课程预先加载Department导航属性。

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

如前文所述,除非是为了提高性能,预先加载不是必须的。由于视图总是需要OfficeAssgnment实体,将它们在同一个查询中进行处理使更有效的。课程实体是在当一个讲师被选择时才需要被加载的,只有页面比没有选择更经常地显示课程,预先加载才比延迟加载更好。

如果一个讲师ID被选择了,会从视图模型的列表中来检索所选择讲师。视图模型的Courses属性通过讲师的Courses导航属性来加载相关的Course实体。

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

Where方法返回一个集合但在这里仅仅是返回一个讲师实体。Single方法将集合转换为一个讲师实体,使您能够访问该实体的Courses属性。

当您知道该集合将只包含一个元素时,您可以使用集合上的Single方法。当你在一个空集合或存有多个元素的集合上调用Single方法时将应发一个异常。另一个选择是使用SingleOrDefault,如果该集合为空,则返回一个缺省值。但在本例中使用SingleOrDefault仍将导致异常(将尝试访问Courses属性,但该属性是一个空引用)并且异常消息会说明这点。当调用Single方法时,您还可以通过传递一个Where条件而不是分别调用Where及Single方法:

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

而不是

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

下一步,如果选择了一个课程,将从视图模型的课程列表中检索所选择的课程。然后从课程的注册导航属性中读取注册实体并加载到到视图模型的注册属性中。

            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 = "Instructor";
}

<h2>Instructor</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

更改了标题

添加了一个办公室列,用来当item.OfficeAssignent不为空的时候显示Location(因为这是一个一对一或零的关系)

            <td>
                @if (item.OfficeAssignment != null)
                {
                    @item.OfficeAssignment.Location
                }
            </td>

通过代码动态的添加class="success"到所选教师的tr元素。这里通过使用Bootstrap样式单来设置已选择行的背景色

        string selectedRow = "";
        if (item.ID == ViewBag.InstructorID)
        {
            selectedRow = "success";
        }
        <tr class="@selectedRow">

添加一个新的ActionLink,用来向Index方法发送所选择的讲师ID

运行应用程序,然后选择讲师选项卡,页面上显示了讲师的信息,以及OfficeAssignment实体不为空情况下的Location属性,如果没有相关的办公室,则什么都不显示。

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

此代码用来读取视图模中的课程属性并显示。它还提供了一个Select超链接,用来将所选课程的ID发送给Index方法。

运行页面并选择一名讲师,你将看到一个表格来显示分配给该讲师的课程。

在您刚才添加的代码之后添加下列代码,用来显示选择的课程中就读的学生列表。

@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Select 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导航属性及每个课程的系属性上您指定了预先加载。然后您将课程集合放到视图模型中,现在你就可以在集合的实体中通过注册导航属性来进行访问。因为你没有指定Course.Enrollments导航属性的预先加载,该属性中的数据将使用延迟加载,只有在呈现页面时才会加载。

如果你禁用延迟加载而不更改其他的代码,则不管实际上有多少注册,Enrollments属性将是空的。在这种情况下,如果想要加载Enrollments属性,你必须指定预先加载或显式加载。你已经见到如何使用预先加载。为了展示显式加载,使用下面的代码替换原先的学生部分,我们将在Enrollments属性上使用显式加载。

            if (courseID != null)
            {
                ViewBag.CourseID = courseID.Value;
                //延迟加载
                //viewModel.Enrollments = viewModel.Courses.Where(
                //    x => x.CourseID == courseID).Single().Enrollments;
                //显式加载
                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;
            }

在选定的Course实体后,新代码使用显式加载课程的Enrollments导航属性:

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

然后显式加载每个Enrollment实体相关的Student实体:

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

请注意你使用Collection方法来加载集合,但对于只有一个实体的属性,使用Reference方法来加载。

现在重新运行页面,确认一切都运行正常,但实际上你已经更改了数据检索的方式。

总结

你现在已经尝试使用延迟、预先及显式三种加载方式来将相关数据加载到导航属性中,在下一节教程中,您将学习如何更新相关的数据。

作者信息

Tom Dykstra - Tom Dykstra 是微软Web平台及工具团队的高级程序员,作家