使用 ASP.NET MVC 应用程序中的实体框架读取相关数据 (5 个,共 10 个)
作者 :Tom Dykstra
Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 创建 ASP.NET MVC 4 应用程序。 若要了解教程系列,请参阅本系列中的第一个教程。
在上一教程中,你已完成学校数据模型。 在本教程中,你将读取和显示相关数据,即实体框架加载到导航属性中的数据。
下图是将会用到的页面。
延迟、预先和显式加载相关数据
实体框架可通过多种方式将相关数据加载到实体的导航属性中:
延迟加载。 首次读取实体时,不检索相关数据。 然而,首次尝试访问导航属性时,会自动检索导航属性所需的数据。 这会导致向数据库发送多个查询 - 一个查询用于实体本身,一次用于必须检索实体的相关数据。
预先加载。 读取该实体时,会同时检索相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 使用
Include
方法指定预先加载。显式加载。 这类似于延迟加载,只不过是在代码中显式检索相关数据;访问导航属性时,它不会自动发生。 可以通过获取实体的对象状态管理器条目并调用
Collection.Load
集合的 方法或Reference.Load
保存单个实体的属性的 方法,手动加载相关数据。 (在以下示例中,如果要加载 Administrator 导航属性,请将 替换为Collection(x => x.Courses)
Reference(x => x.Administrator)
.)
由于它们不会立即检索属性值,因此延迟加载和显式加载也称为 延迟加载。
通常,如果知道需要检索到的每个实体的相关数据,则预先加载可提供最佳性能,因为发送到数据库的单个查询通常比针对检索的每个实体的单独查询更有效。 例如,在上面的示例中,假设每个部门都有 10 门相关课程。 预先加载示例将只导致单个 (联接) 查询和单次往返数据库。 延迟加载和显式加载示例会导致 11 个查询和 11 次往返数据库。 延迟较高时,额外往返数据库对性能尤为不利。
另一方面,在某些情况下,延迟加载效率更高。 预先加载可能会导致生成非常复杂的联接,SQL Server无法高效处理。 或者,如果只需要访问正在处理的一组实体的子集的实体的导航属性,则延迟加载的性能可能更好,因为预先加载会检索比所需更多的数据。 如果看重性能,那么最好测试两种方式的性能,以便做出最佳选择。
通常,只有在关闭延迟加载后,才会使用显式加载。 在序列化期间,应关闭延迟加载的一种方案。 延迟加载和序列化不能很好地混合,如果不小心,在启用延迟加载时,最终查询的数据可能会明显多于预期。 序列化通常通过访问类型实例上的每个属性来工作。 属性访问会触发延迟加载,这些延迟加载的实体将被序列化。 然后,序列化过程访问延迟加载实体的每个属性,这可能会导致更多的延迟加载和序列化。 若要防止这种逃跑链反应,请在序列化实体之前关闭延迟加载。
默认情况下,数据库上下文类执行延迟加载。 可通过两种方式禁用延迟加载:
对于特定导航属性,请在声明属性时省略
virtual
关键字 (keyword) 。对于所有导航属性,请将 设置为
LazyLoadingEnabled
false
。 例如,可以将以下代码放在上下文类的构造函数中:this.Configuration.LazyLoadingEnabled = false;
延迟加载可能会屏蔽导致性能问题的代码。 例如,未指定预先加载或显式加载但处理大量实体并在每次迭代中使用多个导航属性的代码可能非常低效, (因为多次往返数据库) 。 使用本地 SQL Server 在开发中表现良好的应用程序在移动到Azure SQL数据库时可能会遇到性能问题,原因是延迟增加和延迟加载。 使用实际测试负载分析数据库查询将有助于确定延迟加载是否合适。 有关详细信息,请参阅揭秘实体框架策略:加载相关数据和使用实体框架减少SQL Azure的网络延迟。
创建显示系名称的“课程索引”页
Course
实体包括导航属性,其中包含分配有课程的系的 Department
实体。 若要在课程列表中显示分配的部门的名称,需要从Department
导航属性中的Course.Department
实体获取Name
属性。
使用前面Student
为该控制器使用的相同选项为Course
实体类型创建名为 CourseController
的控制器,如下图所示 (除了与图像不同,上下文类位于 DAL 命名空间中,而不是 Models 命名空间) :
打开 Controllers\CourseController.cs 并查看 Index
方法:
public ViewResult 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>
<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>
已对基架代码进行了如下更改:
- 将标题从“索引”更改为“课程”。
- 将行链接向左移动。
- 在标题 “数字 ”下添加了一个显示属性值的
CourseID
列。 (默认情况下,主键不会搭建基架,因为它们通常对最终用户毫无意义。但是,在这种情况下,主键是有意义的,并且你想要显示它。) - 将最后一列标题从 DepartmentID (外键的名称更改为
Department
实体) 部门。
请注意,对于最后一列,基架代码显示 Name
加载到导航属性中的 Department
实体的 Department
属性:
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
运行页面 (选择 Contoso University 主页上的“ 课程 ”选项卡) 以查看具有系名称的列表。
创建显示课程和注册的讲师索引页
在本部分中,你将为 Instructor
实体创建控制器和视图,以显示“讲师索引”页:
该页面通过以下方式读取和显示相关数据:
- 讲师列表显示
OfficeAssignment
实体的相关数据。Instructor
和OfficeAssignment
实体之间存在一对零或一的关系。 将预先加载OfficeAssignment
实体。 如前所述,需要主表所有检索行的相关数据时,预先加载通常更有效。 在这种情况下,你希望显示所有显示的讲师的办公室分配情况。 - 用户选择一名讲师时,显示相关
Course
实体。Instructor
和Course
实体之间存在多对多关系。 对Course
实体及其相关的Department
实体使用预先加载。 在这种情况下,延迟加载可能更有效,因为你只需要所选讲师的课程。 但此示例显示的是如何在本身就位于导航属性内的实体中预先加载导航属性。 - 用户选择一门课程时,会显示
Enrollments
实体集的相关数据。Course
和Enrollment
实体之间存在一对多的关系。 你将为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; }
}
}
为所选行添加样式
若要标记所选行,需要不同的背景色。 若要为此 UI 提供样式,请将以下突出显示的代码添加到 Content\Site.css 中的 部分/* info and errors */
,如下所示:
/* info and errors */
.selectedrow
{
background-color: #a4d4e6;
}
.message-info {
border: 1px solid;
clear: both;
padding: 10px 20px;
}
创建讲师控制器和视图
InstructorController
创建控制器,如下图所示:
打开 Controllers\InstructorController.cs 并为命名空间添加 using
语句 ViewModels
:
using ContosoUniversity.ViewModels;
方法中的 Index
基架代码仅指定导航属性的 OfficeAssignment
预先加载:
public ViewResult 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.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
) 和查询字符串参数 (courseID
) ,该参数提供所选讲师和所选课程的 ID 值,并将所有必需的数据传递到视图。 参数由页面上的“选择”超链接提供。
提示
路由数据
路由数据是模型绑定程序在路由表中指定的 URL 段中找到的数据。 例如,默认路由指定 controller
、 action
和 id
段:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
在以下 URL 中,默认路由映射 Instructor
为 ,映射为 controller
, Index
将 映射到 action
1, id
这些是路由数据值。
http://localhost:1230/Instructor/Index/1?courseID=2021
“?courseID=2021”是查询字符串值。 如果将 作为查询字符串值传递, id
则模型绑定器也会起作用:
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
URL 由 ActionLink
Razor 视图中的 语句创建。 在以下代码中 id
, 参数与默认路由匹配,因此 id
会添加到路由数据中。
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
在以下代码中, courseID
与默认路由中的参数不匹配,因此将其添加为查询字符串。
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
代码先创建一个视图模型实例,并在其中放入讲师列表。 代码指定 和 导航属性的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.InstructorID == id.Value).Single().Courses;
}
方法 Where
返回集合,但在这种情况下,传递给该方法的条件只返回一 Instructor
个实体。 Single
方法将集合转换为单个 Instructor
实体,让你可以访问该实体的 Courses
属性。
如果知道集合将只有一个项,则可以对集合使用 Single 方法。 如果传递给它的集合为空或有多个项,则 Single
方法将引发异常。 另一种方法是 SingleOrDefault,如果集合为空,则返回默认值 (null
在本例中) 。 但是,在这种情况下,仍会导致异常 (尝试在引用) 上null
查找Courses
属性,并且异常消息将不太清楚地指示问题的原因。 调用 Single
方法时,还可以传入 条件, Where
而不是单独调用 Where
方法:
.Single(i => i.InstructorID == id.Value)
而不是:
.Where(I => i.InstructorID == 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>
<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>
@Html.DisplayFor(modelItem => item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
</tr>
}
</table>
已对现有代码进行了如下更改:
将模型类更改为了
InstructorIndexData
。将页标题从“索引”更改为了“讲师” 。
将行链接列移动到左侧。
删除了 FullName 列。
添加了一个 Office 列,该列仅在不为 null 时才
item.OfficeAssignment
显示item.OfficeAssignment.Location
。 (由于这是一对零或一关系,因此可能没有相关的OfficeAssignment
实体。)<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
添加了将动态添加到
class="selectedrow"
tr
所选讲师的 元素的代码。 这会使用之前创建的 CSS 类为所选行设置背景色。 (在valign
将多行列添加到 table 时,在以下教程中,属性将很有用。)string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "selectedrow"; } <tr class="@selectedRow" valign="top">
在每行中的其他链接之前添加了一个新
ActionLink
标签“ 选择 ”,这会导致将所选讲师 ID 发送到Index
方法。
运行应用程序并选择“讲师”选项卡。当没有OfficeAssignment
相关实体时,页面会显示Location
相关OfficeAssignment
实体的属性和一个空的表单元格。
在 Views\Instructor\Index.cshtml 文件中,在文件) 末尾 (结束 table
元素之后,添加以下突出显示的代码。 选择讲师时,将显示与讲师相关的课程列表。
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
</tr>
}
</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
超链接,用于将所选课程的 ID 发送到 Index
操作方法。
注意
.css 文件由浏览器缓存。 如果在运行应用程序时未看到更改,请执行硬刷新 (按住 Ctrl 键的同时单击“ 刷新 ”按钮,或按 Ctrl+F5) 。
运行页面并选择讲师。 此时会出现一个网格,其中显示有分配给所选讲师的课程,且还显示有每个课程的分配系的名称。
在刚刚添加的代码块后,添加以下代码。 选择课程后,代码将显示参与课程的学生列表。
@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>
@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.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 = 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 数据访问内容映射中找到指向其他实体框架资源的链接。