在上一教程中,你已完成 School 数据模型。 在本教程中,您将读取并显示相关数据,即实体框架加载到导航属性中的数据。
下图是将会用到的页面。
Contoso 大学示例 web 应用程序演示了如何使用实体框架 6 Code First 和 Visual Studio 创建 ASP.NET MVC 5 应用程序。 若要了解教程系列,请参阅本系列中的第一个教程。
在本教程中,你将了解:
- 了解如何加载相关数据
- 创建“课程”页
- 创建“讲师”页
一、Prerequisites
二、了解如何加载相关数据
实体框架可以通过多种方式将相关数据加载到实体的导航属性中:
延迟加载。 首次读取实体时,不检索相关数据。 然而,首次尝试访问导航属性时,会自动检索导航属性所需的数据。 这会导致向数据库发送多个查询,一个用于实体本身,每次必须检索到该实体的相关数据。 默认情况下,
DbContext
类启用延迟加载。预先加载。 读取该实体时,会同时检索相关数据。 此时通常会出现单一联接查询,检索所有必需数据。 使用
Include
方法指定预先加载。显式加载。 这类似于延迟加载,只不过是在代码中显式检索相关数据;在访问导航属性时,不会自动发生此情况。 通过获取实体的对象状态管理器条目并调用集合,可以手动加载相关数据。用于集合的 load 方法或用于保存单个实体的属性的load 方法。 (在以下示例中,如果想要加载 "管理员" 导航属性,请将
Collection(x => x.Courses)
替换为Reference(x => x.Administrator)
。)通常,仅当关闭延迟加载时才使用显式加载。
因为它们不会立即检索属性值,所以延迟加载和显式加载也称为延迟加载。
1、性能注意事项
如果知道自己需要每个检索的实体的相关数据,选择预先加载可获得最佳性能,因为相比每个检索的实体的单独查询,发送到数据库的单个查询更加有效。 例如,在上面的示例中,假设每个部门都有十个相关的课程。 预先加载的示例会生成一个(联接)查询和一个到数据库的单次往返。 延迟加载和显式加载示例会导致对数据库进行11次查询和十一次往返。 延迟较高时,额外往返数据库对性能尤为不利。
另一方面,在某些情况下,延迟加载更为有效。 预先加载可能会导致生成非常复杂的联接,这 SQL Server 无法有效地处理。 或者,如果您需要只为正在处理的一组实体的子集访问实体的导航属性,则延迟加载可能会更好,因为预先加载将检索比您所需的数据更多的数据。 如果看重性能,那么最好测试两种方式的性能,以便做出最佳选择。
延迟加载可以屏蔽导致性能问题的代码。 例如,如果代码未指定预先加载或未显式加载,但处理大量实体,并且在每次迭代中使用多个导航属性,则可能会非常低效(因为与数据库之间的往返次数很多)。 使用本地 SQL server 进行开发良好的应用程序在迁移到 Azure SQL 数据库时可能会遇到性能问题,因为这会增加延迟和延迟加载。 使用真实的测试负载分析数据库查询将有助于确定是否适合延迟加载。 有关详细信息,请参阅揭密实体框架策略:加载相关数据和使用实体框架将网络延迟减少到 SQL Azure。
2、在序列化之前禁用延迟加载
如果在序列化过程中使延迟加载处于启用状态,最终可以查询比预期更多的数据。 序列化通常通过访问类型实例上的每个属性来工作。 属性访问会触发延迟加载,并且会序列化这些延迟加载的实体。 然后,序列化过程访问延迟加载的实体的每个属性,这可能会导致更多的延迟加载和序列化。 若要防止此运行时链反应,请在序列化实体之前关闭延迟加载。
序列化还可能会由于实体框架使用的代理类而变得很复杂,如高级方案教程中所述。
避免序列化问题的一种方法是序列化数据传输对象(Dto)而不是实体对象,如使用带有实体框架的 WEB API教程中所示。
如果不使用 Dto,则可以禁用延迟加载,并通过禁用代理创建来避免代理问题。
以下是禁用延迟加载的其他一些方法:
对于特定的导航属性,请在声明属性时省略
virtual
关键字。对于所有导航属性,将
LazyLoadingEnabled
设置为false
,将以下代码放在上下文类的构造函数中:this.Configuration.LazyLoadingEnabled = false;
三、创建“课程”页
Course
实体包含一个导航属性,该属性包含将课程分配到的部门的 Department
实体。 若要在课程列表中显示分配的部门的名称,需要从 Course.Department
导航属性中的 Department
实体获取 Name
属性。
使用之前用于 Student
控制器的实体框架 scaffolder,为 Course
实体类型创建一个名为 CourseController
(不是 CoursesController)的控制器,同时使用与视图相同的 MVC 5 控制器选项:
设置 | “值” |
---|---|
模型类 | 选择课程(ContosoUniversity) 。 |
数据上下文类 | 选择SchoolContext (ContosoUniversity) 。 |
控制器名称 | 输入CourseController。 同样,不CoursesController 。 选择 "课程(ContosoUniversity) " 时,将自动填充 "控制器名称" 值。 必须更改该值。 |
保留其他默认值并添加控制器。
打开ControllersCourseController.cs并查看 Index
方法:
public ActionResult Index()
{
var courses = db.Courses.Include(c => c.Department);
return View(courses.ToList());
}
自动基架使用 Include
方法为 Department
导航属性指定了预先加载。
打开ViewsCourseIndex.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
属性值的“数字”列。 默认情况下,主键不基架,因为它们对于最终用户没有意义。 但在这种情况下主键是有意义的,而你需要将其呈现出来。 - 将 "部门" 列移动到右侧,并更改其标题。 Scaffolder 正确地选择了从
Department
实体显示Name
属性,但在课程页面中,列标题应为 "部门" 而不是 "名称"。
请注意,对于 "部门" 列,基架代码显示加载到 Department
导航属性中的 Department
实体的 Name
属性:
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
运行页面(在 Contoso 大学主页上选择 "课程" 选项卡),查看具有部门名称的列表。
四、创建“讲师”页
在本部分中,你将为 Instructor
实体创建一个控制器和视图,以便显示 "讲师" 页。 该页面通过以下方式读取和显示相关数据:
- 讲师列表显示
OfficeAssignment
实体中的相关数据。Instructor
和OfficeAssignment
实体之间存在一对零或一的关系。 你将对OfficeAssignment
实体使用预先加载。 如前所述,需要主表所有检索行的相关数据时,预先加载通常更有效。 在这种情况下,你希望显示所有显示的讲师的办公室分配情况。 - 用户选择一名讲师时,显示相关
Course
实体。Instructor
和Course
实体之间存在多对多关系。 你将对Course
实体及其相关Department
实体使用预先加载。 在这种情况下,延迟加载可能更高效,因为你只需要为所选指导员提供课程。 但此示例显示的是如何在本身就位于导航属性内的实体中预先加载导航属性。 - 当用户选择一门课程时,将显示
Enrollments
实体集中的相关数据。Course
和Enrollment
实体之间存在一对多的关系。 你将为Enrollment
实体及其相关Student
实体添加显式加载。 (由于启用了延迟加载,因此不需要显式加载,但这会显示如何进行显式加载。)
1、为讲师索引视图创建视图模型
"讲师" 页显示三个不同的表。 因此将创建包含三个属性的视图模型,每个属性都包含一个表的数据。
在viewmodel文件夹中,创建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; }
}
}
2、创建讲师控制器和视图
使用 EF 读取/写入操作创建 InstructorController
(而非 InstructorsController)控制器:
设置 | “值” |
---|---|
模型类 | 选择 "指导员" (ContosoUniversity) 。 |
数据上下文类 | 选择SchoolContext (ContosoUniversity) 。 |
控制器名称 | 输入InstructorController。 同样,不InstructorsController 。 选择 "课程(ContosoUniversity) " 时,将自动填充 "控制器名称" 值。 必须更改该值。 |
保留其他默认值并添加控制器。
打开ControllersInstructorController.cs ,并为 ViewModels
命名空间添加 using
语句:
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
方法将加载课程,对于加载的每个课程,它会预先加载 Course.Department
导航属性。
.Include(i => i.Courses.Select(c => c.Department))
如前所述,无需预先加载,但会执行此操作来提高性能。 由于视图始终需要 OfficeAssignment
实体,因此在同一查询中提取该视图会更有效。 当在网页中选择了指导员时,Course
实体是必需的,因此,只有在选择了比不使用的课程更频繁地显示页面时,预先加载比延迟加载更好。
如果选择了指导员 ID,则会从视图模型中的指导员列表中检索所选的指导员。 然后,将从该教师 Courses
导航属性中的 Course
实体加载视图模型的 Courses
属性。
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
方法会引发异常。 替代项为SingleOrDefault,如果集合为空,则返回默认值(在本例中为null
)。 但是,在这种情况下,仍会引发异常(尝试在 null
引用上查找 Courses
属性),并且异常消息不会明确指出问题的原因。 调用 Single
方法时,还可以传入 Where
条件,而不是单独调用 Where
方法:
.Single(i => i.ID == id.Value)
而不是:
.Where(I => i.ID == id.Value).Single()
接着,如果选择了课程,则从视图模型中的课程列表中检索所选课程。 然后,将通过该课程 Enrollments
导航属性中的 Enrollment
实体加载视图模型的 Enrollments
属性。
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
3、修改讲师索引视图
在ViewsInstructorIndex.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
。将页标题从“索引”更改为了“讲师”。
添加了仅当
item.OfficeAssignment
不为 null 时才显示item.OfficeAssignment.Location
的Office列。 (由于这是一对零或一的关系,因此可能没有相关的OfficeAssignment
实体。)<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
添加了将
class="success"
动态添加到选定讲师的tr
元素中的代码。 这将使用启动类设置所选行的背景色。string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "success"; } <tr class="@selectedRow" valign="top">
添加了在每行中的其他链接之前标记为 "选择" 的新
ActionLink
,这将导致所选的指导员 ID 发送到Index
方法。
运行应用程序并选择 "讲师" 选项卡。当没有相关的 OfficeAssignment
实体时,此页将显示相关 OfficeAssignment
实体的 Location
属性和一个空的表单元。
在ViewsInstructorIndex.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
属性,以显示课程中注册的学生列表。
运行页面并选择一个指导员。 然后选择一门课程,查看参与的学生列表及其成绩。
4、添加显式加载
打开InstructorController.cs ,查看 Index
方法如何获取所选课程的注册列表:
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
检索到讲师列表时,你为 Courses
导航属性指定了预先加载,并为每个课程的 Department
属性指定了预先加载。 然后,将 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 数据访问-建议的资源中找到指向其他实体框架资源的链接。
七、后续步骤
在本教程中,你将了解:
- 已了解如何加载相关数据
- 已创建“课程”页
- 已创建“讲师”页
请继续阅读下一篇文章,了解如何更新相关数据。