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

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

下图是将会用到的页面。

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

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 方法,手动加载相关数据。 (在以下示例中,如果要加载 Administrator 导航属性,请将 替换为 Collection(x => x.Courses)Reference(x => x.Administrator).) 通常,仅当关闭延迟加载时才使用显式加载。

    Explicit_loading_example

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

性能注意事项

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

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

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

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

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

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

避免序列化问题的一种方法是将数据传输对象序列化 (DTO) 而不是实体对象,如 将 Web API 与实体框架配合使用 教程中所示。

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

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

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

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

    this.Configuration.LazyLoadingEnabled = false;
    

创建“课程”页

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

为实体类型创建名为 CourseController (而不是 CoursesController) 的控制器,使用与带视图的 MVC 5 控制器相同的选项,使用之前对Student控制器所做的实体框架基架:Course

设置
Model 类 选择“课程 (ContosoUniversity.Models)
数据上下文类 选择“ SchoolContext (ContosoUniversity.DAL) ”。
控制器名称 输入 CourseController。 同样,不是带有 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 属性值的“数字”列。 默认情况下,主键不会搭建基架,因为它们通常对最终用户毫无意义。 但在这种情况下主键是有意义的,而你需要将其呈现出来。
  • “部门” 列移到右侧,并更改了其标题。 基架正确选择显示实体中的NameDepartment属性,但在“课程”页中,列标题应为 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; }
    }
}

创建讲师控制器和视图

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

设置
模型类 选择“讲师 (ContosoUniversity.Models)
数据上下文类 选择“ SchoolContext (ContosoUniversity.DAL) ”。
控制器名称 输入 InstructorController。 同样,不是具有 s 的InstructorsController。 如果选择“ 课程 (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.Courses预先加载Instructor.OfficeAssignment

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,它都会预先加载 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 在本例中) 。 但是,在这种情况下,仍会导致异常 (尝试在引用) 上null查找Courses属性,并且异常消息将不太清楚地指示问题的原因。 调用 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

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

  • 添加了一个 Office 列,该列仅在不为 null 时才item.OfficeAssignment显示item.OfficeAssignment.Location。 (由于这是一对零或一关系,因此可能没有相关的 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 数据访问 - 推荐资源中找到指向其他实体框架资源的链接。

后续步骤

在本教程中,你将了解:

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

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