读取关联数据 - ASP.NET Core MVC 与 EF Core 教程 (6 of 10)
作者 Tom Dykstra 、 Rick Anderson
Contoso 大学 Web应用程序演示了如何使用 Entity Framework Core 1.1 以及 Visual Studio 2017 来创建 ASP.NET Core 1.1 MVC Web 应用程序。更多信息请参考 第一节教程.
在上一篇教程中,你已经完成了 School 数据模型的设计。在本教程中你将读取并显示关联数据——也就是说由 Entity Framework 加载到导航属性中的数据。
下图展示了你将使用到的页面:
对关联数据的预加载、显式加载与延迟加载
对象关系映射(ORM)软件(例如 Entity Framework)可以通过多种途径加载关联数据到某实体的导航属性中:
预加载(Eager Loading)。当实体被读取时,关联数据与其一道被检索。这通常会出现在通过单个连接查询检索所有所需数据的情况中。你在使用 Entity Framework Core 的
Include
和ThenInclude
方法时就会指定使用预加载。你可以在单独的查询中检索一些数据,这样 EF 会「修复」导航属性。这句话的意思是说,在之前检索得出的实体中,EF 会自动为其中的导航属性单独检索,并将所得实体添加到这些导航属性中。对于检索关联数据的查询,你可以使用
Load
m方法来代替ToList
或Single
方法(后者会返回一组或单个结果)。显示加载(Explicit Loading)。当实体首次读取时,关联数据并没有被检索出来。关联数据由你自己编写代码来检索。与预加载中加载单独查询相比,显式加载通过向数据库发送多个查询来加载结果。两者的区别在于,在使用显式加载时,需要用代码将数据加载到导航属性中。Entity Framework Core 1.0 不提供显式加载 API。
延迟加载(Lazy Loading)。当实体首次读取时,关联属性不会被检索。当首次尝试访问导航属性时,将自动检索该导航属性所需的数据。每次尝试获取数据时,只要是首次获取数据都将向数据库发出一次查询。Entity Framework Core 1.0 不支持延迟加载。
性能问题
如果你知道你每个检索的得到的实体都需要关联数据,那么使用预加载(Eager Loading)会性能更好,因为向数据库发送单个查询总比每个检索所得的实体单独向数据库发送请求来得高效。举个例子,假设每个部门有十个与之关联的课程。所有关联数据使用预加载去检索的话,只需要一次(连接)查询、往返数据库一次即可。如果是针对每门课程单独查询,那么将导致十一次往返。当延迟较高时,对数据库的额外往返会对访问性能产生不利影响。
另一方面,在某些情况下单独查询效率则会更高。在一个查询中预加载所有关联数据会导致生成非常复杂的、连 SQL Server 对此的处理都不甚高效的连接语句。或者如果你只需要查询某个实体(正在处理的实体集)的导航属性(该实体集的子集),单独查询的执行效率可能会更好一些,因为事先加载所有数据的预加载对你而言显然数据量显得有些多。如果性能对你很重要,最好对两种方式进行对比测试,选出性能最佳者。
创建显示 Department 名称的 Courses 页
Course 实体所包含的导航属性中含有课程所分配到的部门的 Department 实体。为了显示这些课程所分配到的部门的名称,你需要从 Department 实体中获取 Name 属性,该属性位于 Course.Department
导航属性之中。
为 Course 实体类型创建名为 CoursesController 的控制器,使用与先前你为 Students 控制器一样配置的使用 Entity Framework、带有视图的 MVC 控制器基架,如下图所示:
打开 CourseController.cs 文件,检查 Index
方法。自动化的基架使用 Include
方法来指定针对 Department
导航属性的加载为预加载方式。
用以下代码替换 Index
方法,并使用一个更合适的名称为 IQueryable
回 Course 实体(用 courses
代替 schoolContext
):
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}
打开 Views/Courses/Index.cshtml 文件,用以下代码代替模板代码。改动高亮部分:
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@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>
<a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
如此,你针对基架代码做了如下变更:
将标题从 Index 改成了 Courses。
增加了一个用于显示
CourseID
属性值的 Number 列。默认情况下,并不显示主键,因为通常情况下队最终用户无意义。不过在本例中主键是有意义的,所以你想把它显示出来。添加 Department 列。注意 Department 列显示的是被加载到
Department
导航属性的 Department 实体的Name
属性:@Html.DisplayFor(modelItem => item.Department.Name)
运行页面(在 Contoso University 首页选择 Courses 标签),查看部门名称列表。
创建展示 Courses 和 Enrollments 的 Instructors 页
在本节中你将学习为 Instructor 实体创建控制器和视图,用于显示 Instructor 页面:
该页面以以下方式读取并显示关联数据:
教师列表中显示来自 OfficeAssignment 实体的关联数据。Instructor 和 OfficeAssignment 实体之间的关系是一对零或一的关系。你需要为 OfficeAssignment 使用预加载的方式。如前所述,在你需要为所有从主表中检索出的结果添加关联数据的时候,预加载方式通常会比较高效。在本例中,你希望显示所有教师分配的办公室。
当用户选择教师时,与之相关的 Course 实体就会被显示。Instructor 和 Course 实体之间是多对多的关系。你将使用预加载的方式加载 Course 实体以及它相关的 Department 实体。在本例中,单独查询的性能会更好一些,因为你需要的课程信息只是为了选择教师。不过在本例中演示了如何利用预加载的方法加载自己就在当行属性的实体内的导航属性的数据
当用户选择一门课程后,来自 Enrollments 实体的关联数据被显示出来。Course 和 Enrollment 实体之间是一对多的关系。你将使用单独查询的方法检索 Enrollment 实体以及与之相关的 Student 实体。
创建用于 Instructor 索引视图的视图模型
Instructors 页面中显示的数据来自三张不同的表。因此,你需要创建一个包含三个属性的视图,每一个属性对用一张表的数据。
在 SchoolViewModels 文件夹中创建 InstructorIndexData.cs 文件,并用以下代码替换已存在的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
创建 Instructor 控制器和视图
创建一个带有 EF 读写操作的 Instructors 控制器,如下图所示:
打开 InstructorsController.cs 文件并添加添加一句 using 语句来启用视图模型的命名空间:
using ContosoUniversity.Models.SchoolViewModels;
用以下代码替换 Index 方法中的代码,这段代码将预加载关联数据并将之放入视图模型中。
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
方法接受可选的代表所选教师 ID 值的路由数据(id
)以及代表所选课程 ID 值的查询字符串参数(courseID
)。这些参数都由页面上的 Select 超链所提供。
这段代码首选创建了一个视图模型的示例,而后将教师列表放入其中。代码为 Instructor.CourseAssignments
和Instructor.OfficeAssignment
导航属性指定了预加载的方法。在 CourseAssignments
属性中, Enrollments
和 Department
属性都被加载了,在每一个 Enrollment
实体中e Student
属性也同样被加载了。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
由于视图一直需要 OfficeAssignment 实体,因此在同一个请求中提取它会更有效率。当在页面中选中一个教师时需要 Course 实体,所以当页面会比较频繁地需要显示课程数据,那么单一查询明显优于多个查询。
代码中的 CourseAssignments
和 Course
重复了,因为您需要 Course
中的两个属性。 ThenInclude
的第一个调用得到CourseAssignment.Course
, Course.Enrollments
, 和 Enrollment.Student
.。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
在代码中的那块上,另一个 ThenInclude
将用于 Student
的导航属性,您不需要它。 但是,通过 Instructor
属性调用 Include
就可以重新开始,所以你必须重新遍历链接,这次指定 Course.Department
而不是Course.Enrollments
。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
当选择了教师时,执行以下代码。 从视图模型中的教师列表中检索选定的讲师。 视图模型的 Courses
属性随后由教师的 CourseAssignments
导航属性加载课程实体。
#endregion
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
Where
方法返回一个集合,但在本例中该方法的条件只会返回一个 Instructor 实体。 Single
方法将集合转换为一个单独的 Instructor 实体,供你访问该实体中的 CourseAssignments
属性。 CourseAssignments
属性包含 CourseAssignment
实体,其中只有你所需的与该数据相关联的 Course
实体。
当你知道集合中将只有一项时,你可以对该集合使用 Single
方法。当传入的集合为空(empty)或大于一条记录时,Single 方法会抛出异常。另一种方法叫 SingleOrDefault
,当传如入的集合为空(empty)时返回默认值(本例中为 null)。不过在本例中此处依旧会抛出异常(试图在空引用(null reference)中查找 CourseAssignments
属性),并且异常消息(exception message)也会很不清晰地指示导致该问题的原因。当你调用 Single
方法时,你可以传入 Where 条件,而不是去分别调用 Where
方法。比如用这段代码:
.Single(i => i.ID == id.Value)
取代这段代码:
.Where(I => i.ID == id.Value).Single()
下一步,如果选中一门课程,则从视图模型的课程列表中检索该门课程。然后视图模型的 Enrollments
属性被加载,其所加载的 Enrollment 实体来自被选课程的导航属性 Enrollments
。
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
修改 Instructor 索引视图
在 Views/Instructor/Index.cshtml 中用下列代码替换模板代码。修改部分为高亮代码。
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["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>
@{
foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @: @course.Course.Title <br />
}
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
你此时已对代码做了如下变更:
将模型类改成了
InstructorIndexData
。将页面标题从 Index 改成了 Instructors。
添加了一个 Office 列,当且仅当
item.OfficeAssignment.Location
为 null 时方才显示item.OfficeAssignment
(因为这是一对零或一(one-to-zero-or-one)关系,存在没有相关联的 OfficeAssignment 实体的可能)。@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
- 添加 Courses 列用于显示每位教师所教授的课程。
添加一段能动态添加
class="success"
到所选教师的tr
元素中的代码块。该 CSS 样式用于将被选中行的背景颜色设置为一个 Bootstrap 类。string selectedRow = ""; if (item.ID == (int?)ViewData["InstructorID"]) { selectedRow = "success"; }
在每行其他链接前添加一个新的标记为 Select 的超链,用于向
Index
方法发送所选教师的 ID。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
运行应用程序并选择 Instructors 标签。当无任何关联 OfficeAssignment 实体时,页面将显示关联 OfficeAssignment 实体的 Location 属性以及空的表格单元格。
在 Views/Instructors/Index.cshtml 文件中,在表格元素之后(也就是这个文件的最后),添加下面这段代码。这段代码的作用是当选择一个教师时,显示该教师关联的课程清单。
@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 == (int?)ViewData["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 超链用于像 Index
操作方法发送被选课程的 ID。
运行页面并选择一位教师。此刻你能看到一张显示有被分配给所选教师的课程的表格,以及每门课程被分配的部门的名称。
在你刚才所添加的代码块的后面再添加下面这段代码。这段代码的作用是:当你选择了一门课程时,显示这门课程的注册学生的列表。
@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 属性,以便在课程中显示注册学生的列表。
运行页面,选择一个教师。然后选择一门课程来查看已注册学生的名单及其成绩。
显式加载
当你在InstructorsController.cs 中检索教师列表时,你需要为明确地预加载 CourseAssignments
导航属性。
假设你希望让用户只在选定一门课程和一个教师时才能看到注册。在该种情形下,你可能希望仅在请求时加载注册数据。要查看如何进行显式加载的示例,请使用以下代码替换 Index
方法,这将删除主动加载并显式加载该属性。 代码修改部分高亮显示。
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
新代码从检索 Instructor 实体的代码中删除了对数测数据的 ThenInclude 的方法调用。如果教师和课程都被选择,高亮代码将检索所选课程的 Enrollment 实体。依据这些 Enrollment 实体,代码会预加载 Student 导航属性。
运行 Instructor 索引页,你能看到页面山显示的内容没啥区别,但实际上你已经改变了检索数据的方式。
总结
你现在已经对单一查询和多查询使用预加载来读取关联数据到导航属性。下一篇教程你讲学习如何更新关联数据。