MVC中使用EF(五):在 ASP.NET MVC 程序使用Entity Framework读取关联数据
在 ASP.NET MVC程序使用Entity
Framework读取关联数据
Contoso University示例网站演示如何使用Entity Framework 5创建ASP.NET MVC 4应用程序。Entity Framework有三种处理数据的方式: Database First, Model First, and Code First. 本指南使用代码优先。其它方式请查询资料。示例程序是为Contoso University建立一个网站。功能包括:学生管理、课程创建、教师分配。 本系列指南逐步讲述如何实现这一网站程序。
本示例程序基于 ASP.NET MVC.如果使用 ASP.NET Web Forms model, 请查看 Model Binding and Web Forms系列指南和 ASP.NET Data Access Content Map.
如有问题,可在这些讨论区提问: ASP.NET Entity Framework forum, the Entity Framework and LINQ to Entities forum, or StackOverflow.com.
在上一节完成了School数据模型。本节将读取、显示相关联数据,也就是说使用EF加载导航属性数据。
你将完成的页面效果如下:
延迟,渴望,显式加载关联数据
EntityFramework 加载实体的导航属性数据有多种方法:
-
延迟加载.第一次读取实体时关联数据不检索。在第一次读取导航属性时,自动检索相应数据。这将导致向数据库发出多次查询请求—请求实体本身、请求相关导航数据。
-
渴望加载.关联数据和实体同时读取。这样一次链接查询即可获取所有需要的数据。使用 Include方法实现渴望加载.
-
显式加载.和延迟加载相似,但是由你通过代码明确检索关联数据;This is similar tolazy loading, except that you explicitly retrieve the related data in code; itdoesn't happen automatically when you access a navigation property.你需要手动调用实体的入口(Entry)的 Collection.Load 方法加载集合导航属性、 Reference.Load 加载单个导航属性. (下面的代码中,如果你想加载Administrator属性的数据,把 Collection(x => x.Courses) 替换成Reference(x =>x.Administrator).)
延迟加载和显式加载通称为 延期加载.
通常,如果需要每个实体的关联数据,渴望加载性能最好,对数据库做一次查询请求比分散多次请求效率要高。比如在上面的例子中,假设每个部门有十门相关课程。渴望加载通过一次连接查询对数据库做一轮搜索即可获取所有数据。延迟加载和显式加载都将产生十一次查询,对数据库做十一轮搜索。性能将受到影响。
但在某些场景下延迟加载效率更高。渴望加载可能导致产生很复杂的连接查询,以至于SQL Server不能高效处理。或者如果你只是访问一个实体集中部分实体的导航属性,延迟加载也可能比渴望加载性能要好,因为后者会检索的数据超出你所需要的。如果性能很重要,最好测试两种方式的性能来选择使用哪一个最好。
一般只有在关闭延迟加载的时候才会使用显式加载。比如在序列化期间应该关闭延迟加载。延迟加载和序列化协同效果并不好,如果不小心可能会导致查询数据远远超出你想要的。序列化通过访问一个类型实例的每个属性完成工作。序列化过程将访问延迟加载实体的每一个属性,可能导致更多延迟加载和序列化。为了阻止这种连锁反应,在序列化实体之前应关闭延迟加载。
数据上下文默认使用延迟加载。禁用延迟加载有两种方式:- 对特定导航属性在声明时略去virtual 关键词.
-
对所有导航属性, 设置 LazyLoadingEnabled 为 false.例如,可在数据上下文构造函数中添加如下代码:
this.Configuration.LazyLoadingEnabled = false;
延迟加载可能导致性能问题。例如,没有指明渴望或显式加载但处理了大批量实体,在每一次循环中使用了一些导航属性会导致效率低下(因为对数据库的多轮搜索)。在开发时性能良好的程序在移植到Windows Azure SQLDatabase之后因为延迟可能导致性能问题。需要在实际测试中验证延迟加载是否合适.更多信息请查看 Demystifying Entity Framework Strategies: Loading Related Data 和 Using the Entity Framework to Reduce Network Latency to SQL Azure.
在 Courses Index 页面显示 Department Name
Course 实体包含 Department 导航属性,包含课程所属部门的信息, 需要从 Course.Department 导航属性读取部门的名称.
为Course 创建 CourseController 控制器,选项如下:
打开 Controllers\CourseController.cs 查看 Index 方法代码:
public ViewResult Index() { var courses = db.Courses.Include(c => c.Department); return View(courses.ToList()); }
自动生成的代码采用渴望加载的方式获取 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>
对自动生成的代码做了如下修改:
- 标题从 Index 改为 Courses.
- 链接移到左侧.
-
添加 Number 列显示 CourseID 属性值. (默认自动代码不包含主键信息,通常主键信息没有具体含义。但本例中,主键信息是有必要显示的.)
- 把 DepartmentID 列( Department 实体的外键)修改为 Department.
注意,自动生成的代码在这一列显示的是Department 导航属性相应实体的 Name 属性:
<td> @Html.DisplayFor(modelItem => item.Department.Name) </td>
运行程序查看效果.
创建 Instructors Index页面,显示 Courses和 Enrollments
本小节你将为Instructor 实体创建控制器和视图:
本页采用如下方式访问相关联数据:
- instructors列表显示来自 OfficeAssignment 实体的信息. Instructor 和OfficeAssignment 实体是1对0或1的关系.使用渴望加载的方式获取OfficeAssignment 实体.上面已经说过,渴望加载方式对访问主表每条记录的导航属性更有效率。本例中,希望显示每位老师所在办公室的信息.
- 当选择了某instructor, 相关 Course 实体将显示.Instructor 和 Course实体是多对多关系.使用渴望加载获取 Course 实体及其相关Department 实体.本例中, 延迟加载可能更有效,因为只显示选中记录的相关信息.不过这里是为了演示如何使用渴望加载获取导航属性实体中的导航属性.
-
当选择了课程,相应 Enrollments 信息将显示. Course 和Enrollment 实体是1对多的关系.使用显式加载获取 Enrollment 实体及其相关的 Student 实体. (延迟加载启用时,显式加载没有必要,这里只是为了演示如何使用)
为 Instructor Index视图创建视图模型
Instructor Index页面显示了三个不同的表.因此应创建视图模型包含这三个表,每个属性对应一个表.
在 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; } } }
为选中行添加样式
在 Content\Site.css文件的 /* info and errors */ 部分,添加如下样式:
/* info and errors */ .selectedrow { background-color: #a4d4e6; } .message-info { border: 1px solid; clear: both; padding: 10px 20px; }
创建 Instructor控制器和视图
创建 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),由此确定应反馈给视图的数据.参数是由 页面的链接传递.
代码首先获取Instructor列表,渴望加载模式获取 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 加载Course.Department 导航属性.
.Include(i => i.Courses.Select(c => c.Department))
因为一直需要OfficeAssignment 实体,渴望加载方式比较高效。 Course 实体在教师被选中后显示,因此渴望加载只有在教师被频繁选中时才比延迟加载高效。
如果某一instructor ID 被选中,被选中的 instructor从视图模型的instructors列表检索出来。视图模型的 Courses 属性值由此instructor的 Courses导航属性获取.
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 方法.如果集合为空或者对象数多于1, Single方法将抛出异常.可使用 SingleOrDefault,如果集合为空将返回默认值 (本例中为null).但是,本例中依然会异常(尝试从null 引用获取Courses 属性),.调用 Single 方法时可以传入 Where 条件,这样不用再调用 Where 方法了:
.Single(i => i.InstructorID == id.Value)
而无需:
.Where(I => i.InstructorID == id.Value).Single()
如果选中某课程,选中的课程将从视图模型的课程列表中检索出来。然后使用其 Enrollments导航属性赋给视图模型的Enrollments 属性。
if (courseID != null) { ViewBag.CourseID = courseID.Value; viewModel.Enrollments = viewModel.Courses.Where( x => x.CourseID == courseID).Single().Enrollments; }
修改Instructor Index View
在 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.
- 标题从 Index 改为 Instructors.
- 链接移到左侧.
- 移除 FullName 列.
- 添加 Office 列,当item.OfficeAssignment不为null时显示 item.OfficeAssignment.Location
-
<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
-
添加为选中的记录设置样式的代码 class="selectedrow"
string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "selectedrow"; } <tr class="@selectedRow" valign="top">
- 在每行其它链接之前添加名为Select 的 ActionLink ,将选中记录的ID传递到Index 方法.
运行,查看效果。
在 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 键点击Refresh 按钮, 或使用 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; }
在检索教师列表的时候,渴望加载模式检索了 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.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 实体之后, t显式加载课程的 Enrollments 导航属性
db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
显示加载 Enrollment 实体相关的 Student 实体:
db.Entry(enrollment).Reference(x => x.Student).Load();
注意使用 Collection 方法加载集合属性,Reference 方法加载单个实体属性.
运行查看效果.总结
使用三种方式(延迟、渴望、显式)加载相关的导航属性。下一节将学习如何更新相关联的数据。
其它 EntityFramework资源请查看 ASP.NET Data Access Content Map.