教程:在 ASP.NET MVC 应用中使用 EF 读取相关数据
在上一教程中,你已完成学校数据模型。 在本教程中,你将读取和显示相关数据,即 Entity Framework 加载到导航属性中的数据。
下图是将会用到的页面。
Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 6 Code First 和 Visual Studio 创建 ASP.NET MVC 5 应用程序。 若要了解系列教程,请参阅本系列中的第一个教程。
在本教程中,你将了解:
- 了解如何加载相关数据
- 创建“课程”页
- 创建“讲师”页
先决条件
了解如何加载相关数据
实体框架可通过多种方式将相关数据加载到实体的导航属性中:
延迟加载。 首次读取实体时,不检索相关数据。 然而,首次尝试访问导航属性时,会自动检索导航属性所需的数据。 这会导致发送到数据库的多个查询 - 一个用于实体本身,每次必须检索实体的相关数据时, 一个查询。 默认情况下,该
DbContext
类启用延迟加载。预先加载。 读取该实体时,会同时检索相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 使用该方法指定预先加载
Include
。显式加载。 这类似于延迟加载,只是你在代码中显式检索相关数据;访问导航属性时,它不会自动发生。 通过获取实体的对象状态管理器条目并调用 集合的 Collection.Load 方法或保存单个实体的属性的 Reference.Load 方法,手动加载相关数据。 (在以下示例中,如果要加载管理员导航属性,则
Collection(x => x.Courses)
替换为Reference(x => x.Administrator)
.)通常,只有在关闭延迟加载后,才会使用显式加载。
由于它们不会立即检索属性值,延迟加载和显式加载也称为 延迟加载。
性能注意事项
如果知道自己需要每个检索的实体的相关数据,选择预先加载可获得最佳性能,因为相比每个检索的实体的单独查询,发送到数据库的单个查询更加有效。 例如,在上述示例中,假设每个部门都有十个相关课程。 预先加载示例只会导致单个(联接)查询和数据库的单个往返。 延迟加载和显式加载示例都会导致 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
实体的相关数据。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; }
}
}
创建讲师控制器和视图
使用 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.OfficeAssignment
Instructor.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 资源的链接。
后续步骤
在本教程中,你将了解:
- 已了解如何加载相关数据
- 已创建“课程”页
- 已创建“讲师”页
请继续阅读下一篇文章,了解如何更新相关数据。