教程:在 ASP.NET MVC 应用中使用 EF 读取相关数据

在上一教程中,你已完成学校数据模型。 在本教程中,你将读取和显示相关数据,即 Entity Framework 加载到导航属性中的数据。

下图是将会用到的页面。

显示“课程”页的屏幕截图,其中包含课程列表。

Instructors_index_page_with_instructor_and_course_selected

下载已完成的项目

Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 6 Code First 和 Visual Studio 创建 ASP.NET MVC 5 应用程序。 若要了解系列教程,请参阅本系列中的第一个教程

在本教程中,你将了解:

  • 了解如何加载相关数据
  • 创建“课程”页
  • 创建“讲师”页

先决条件

实体框架可通过多种方式将相关数据加载到实体的导航属性中:

  • 延迟加载。 首次读取实体时,不检索相关数据。 然而,首次尝试访问导航属性时,会自动检索导航属性所需的数据。 这会导致发送到数据库的多个查询 - 一个用于实体本身,每次必须检索实体的相关数据时, 一个查询。 默认情况下,该 DbContext 类启用延迟加载。

    Lazy_loading_example

  • 预先加载。 读取该实体时,会同时检索相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 使用该方法指定预先加载 Include

    Eager_loading_example

  • 显式加载。 这类似于延迟加载,只是你在代码中显式检索相关数据;访问导航属性时,它不会自动发生。 通过获取实体的对象状态管理器条目并调用 集合的 Collection.Load 方法或保存单个实体的属性的 Reference.Load 方法,手动加载相关数据。 (在以下示例中,如果要加载管理员导航属性,则 Collection(x => x.Courses) 替换为 Reference(x => x.Administrator).)通常,只有在关闭延迟加载后,才会使用显式加载。

    Explicit_loading_example

由于它们不会立即检索属性值,延迟加载和显式加载也称为 延迟加载

性能注意事项

如果知道自己需要每个检索的实体的相关数据,选择预先加载可获得最佳性能,因为相比每个检索的实体的单独查询,发送到数据库的单个查询更加有效。 例如,在上述示例中,假设每个部门都有十个相关课程。 预先加载示例只会导致单个(联接)查询和数据库的单个往返。 延迟加载和显式加载示例都会导致 11 个查询和 11 次往返数据库。 延迟较高时,额外往返数据库对性能尤为不利。

另一方面,在某些情况下,延迟加载效率更高。 预先加载可能会导致生成非常复杂的联接,SQL Server 无法高效处理。 或者,如果需要仅访问一组正在处理的实体的子集的实体的导航属性,则延迟加载的性能可能会更好,因为急切的加载将检索比所需的数据更多的数据。 如果看重性能,那么最好测试两种方式的性能,以便做出最佳选择。

延迟加载可能会屏蔽导致性能问题的代码。 例如,不指定预先加载或显式加载但处理大量实体并在每次迭代中使用多个导航属性的代码可能非常低效(因为多次往返数据库)。 使用本地 SQL Server 在开发中表现良好的应用程序在迁移到Azure SQL 数据库时可能会遇到性能问题,因为延迟和延迟加载增加。 使用实际测试负载分析数据库查询将有助于确定延迟加载是否合适。 有关详细信息,请参阅 揭秘实体框架策略:加载相关数据 并使用 Entity Framework 来降低 SQL Azure 的网络延迟。

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

如果在序列化期间保持延迟加载,最终查询的数据量比预期要多得多。 序列化通常通过访问类型实例上的每个属性来工作。 属性访问会触发延迟加载,并序列化这些延迟加载的实体。 然后,序列化过程将访问延迟加载的实体的每个属性,这可能会导致更延迟的加载和序列化。 若要防止出现此逃跑链反应,请在序列化实体之前关闭延迟加载。

如高级方案教程中所述,实体框架使用的代理类也可能会使序列化变得复杂。

避免序列化问题的一种方法是序列化数据传输对象(DTO),而不是实体对象,如在 Entity Framework 中使用 Web API 教程中所示

如果不使用 DTO,可以通过禁用代理创建来禁用延迟加载并避免代理问题

下面是禁用延迟加载的一些其他方法:

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

  • 对于所有导航属性,设置为 LazyLoadingEnabled false,请将以下代码放在上下文类的构造函数中:

    this.Configuration.LazyLoadingEnabled = false;
    

创建“课程”页

Course 实体包括导航属性,其中包含分配有课程的系的 Department 实体。 若要在课程列表中显示分配的部门的名称,需要从Department导航属性中的Course.Department实体获取Name该属性。

为实体类型创建一个名为CourseController(不是 CoursesController)的Course控制器,对具有视图的 MVC 5 控制器使用相同的选项,并使用之前为Student控制器创建的 Entity Framework 基架:

设置 “值”
Model 类 选择课程(ContosoUniversity.Models)。
数据上下文类 选择 SchoolContext (ContosoUniversity.DAL)。
控制器名称 输入 CourseController。 同样,不是具有 s 的 CoursesController。 选择 Course (ContosoUniversity.Models)时, 控制器名称 值会自动填充。 必须更改值。

保留其他默认值并添加控制器。

打开 Controllers\CourseController.cs 并查看 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>

已对基架代码进行了如下更改:

  • 将标题从“索引”更改为“课程”。
  • 添加了显示 CourseID 属性值的“数字”列。 默认情况下,主键不是基架,因为它们通常对最终用户毫无意义。 但在这种情况下主键是有意义的,而你需要将其呈现出来。
  • 将“ 部门 ”列移动到右侧并更改了其标题。 基架正确选择显示实体中的Department属性,但在“课程”页中,列标题应为“部门”而不是“名称”。Name

请注意,对于“部门”列,基架代码显示Name加载到导航属性中的Department实体的属性Department

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

运行页面(选择 Contoso University 主页上的“课程 ”选项卡),查看具有部门名称的列表。

创建“讲师”页

在本部分中,你将为 Instructor 实体创建控制器和视图,以显示“讲师”页。 该页面通过以下方式读取和显示相关数据:

  • 讲师列表显示 OfficeAssignment 实体的相关数据。 InstructorOfficeAssignment 实体之间存在一对零或一的关系。 将预先加载 OfficeAssignment 实体。 如前所述,需要主表所有检索行的相关数据时,预先加载通常更有效。 在这种情况下,你希望显示所有显示的讲师的办公室分配情况。
  • 用户选择一名讲师时,显示相关 Course 实体。 InstructorCourse 实体之间存在多对多关系。 对 Course 实体及其相关的 Department 实体使用预先加载。 在这种情况下,延迟加载可能更有效,因为你只需要为所选讲师提供课程。 但此示例显示的是如何在本身就位于导航属性内的实体中预先加载导航属性。
  • 用户选择一门课程时,会显示 Enrollments 实体集的相关数据。 CourseEnrollment 实体之间存在一对多的关系。 你将为 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; }
    }
}

创建讲师控制器和视图

使用 EF 读取/写入操作创建( InstructorController 而不是 InstructorsController)控制器:

设置 “值”
Model 类 选择讲师(ContosoUniversity.Models)。
数据上下文类 选择 SchoolContext (ContosoUniversity.DAL)。
控制器名称 输入 InstructorController。 同样,不是使用 s 的 InstructorsController。 选择 Course (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),该参数提供所选讲师和所选课程的 ID 值,并将所有必需的数据传递给视图。 参数由页面上的“选择”超链接提供。

代码先创建一个视图模型实例,并在其中放入讲师列表。 该代码指定预先加载和Instructor.OfficeAssignmentInstructor.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 在网页中选择讲师时,实体是必需的,因此,仅当页面显示频率高于未选择的课程时,预先加载比延迟加载更好。

如果选择了讲师 ID,则从视图模型中的讲师列表中检索所选讲师。 然后向视图模型的 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的属性随该课程导航属性中的Enrollments实体一起Enrollment加载。

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

  • 将页标题从“索引”更改为了“讲师” 。

  • 添加了仅当不为 null 时item.OfficeAssignment显示的 item.OfficeAssignment.Location Office 列。 (由于这是一对零或一种关系,因此可能不存在相关 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标签,这会导致所选的讲师 ID 发送到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 超链接,用于将所选课程的 ID 发送到 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;
}

检索讲师列表时,指定了导航属性和Department每个课程的属性的预先加载Courses。 然后将集合放入 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();

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

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

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

立即运行“讲师索引”页,尽管你更改了数据的检索方式,但页面上显示的内容没有任何差异。

获取代码

下载已完成的项目

其他资源

可以在 ASP.NET 数据访问 - 建议的资源中找到 指向其他 Entity Framework 资源的链接。

后续步骤

在本教程中,你将了解:

  • 已了解如何加载相关数据
  • 已创建“课程”页
  • 已创建“讲师”页

请继续阅读下一篇文章,了解如何更新相关数据。