By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.
全文目录:Contoso 大学 - 使用 EF Code First 创建 MVC 应用
在前面的课程中已经完成了 School 数据模型。在这次的课程中,将要读取和显示相关的数据,这里指的是 EF 通过导航属性加载的数据。
下面的截图展示了你将好创建的页面。
EF 有多种方式可以通过导航属性加载关联的数据。
因为不会立即获取关联属性的值,延迟加载和显式加载又被称为延后加载。
一般来说,如果你知道你需要每个实体的关联属性,饿汉加载提供了最好的性能。因为只有一次查询被发送到数据库,比对每个实体都要向数据库发出一次查询要更加有效。例如,在上面的例子中,假设每个系都有相关的课程,饿汉加载只需要一次联合查询就可以获得。而使用延迟加载或者显式加载则需要 11 次查询。
从另外的角度来说,如果你不常访问实体的导航属性,或者仅仅访问一小部分实体的导航属性,延迟加载更加有效,因为饿汉加载会加载更多地不必要的数据。通常情况下,在关闭了延迟加载的情况下使用显式加载。一个关闭延迟加载的场景是在进行序列化的时候,当你知道不需要所有的导航属性数据加载。如果延迟加载启用,所有的导航属性将会自动加载,因为序列化会访问所有的属性。
数据库上下文默认支持延迟加载,有两种方法可以关闭延迟加载:
延迟加载可能导致性能问题,例如,代码中没有指定使用饿汉加载或者显式加载,但是在处理大量实体的时候,遍历每个实体并访问其导航属性可能导致低效率 ( 因为多次访问数据库 ), 但是使用延迟加载不会出现问题。在代码使用延迟加载的时候临时禁用延迟加载可能导致出现问题。因为导航属性为 null 而导致代码访问对象失败。
课程 Course实体包含一个所属系 Department 的导航属性,为了显示课程所属系的名称,你需要通过课程所属的系 Department 导航属性来获取系的名称 Name。
为课程实体 Course 创建一个控制器,使用与前面的学生 Student 相同的设置,如下图所示:
打开 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>
这段代码对脚手架代码做了如下的修改:
注意,脚手架代码显示通过导航属性 Department 加载的系实体的 Name 属性值。
<td> @Html.DisplayFor(modelItem => item.Department.Name) </td>
重新运行这个页面,( 在 Contoso 大学的首页中选择 Courses )来显示系名称的列表。
在这一节中,我们创建控制器和视图来显示教师实体。
这个页面使用下面的途径来读取和显示关联的数据:
教师页面显示三个不同的表。因此,需要创建一个新的视图模型,通过三个属性表示出来,每一个持有一张表的数据。
在 ViewModels 文件夹中,创建 InstructorIndexData.cs ,将生成的代码替换为以下代码。
using System; 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 文件中标记为 MISC 的节中,如下所示。
/* MISC ----------------------------------------------------------*/ .selectedrow { background-color: #EEEEEE; }
为教师实体类型创建一个控制器。使用类似前面 Student 控制器的方式创建,如下所示:
打开 Controllers\InstructorController.cs ,为 ViewModels 命名空间增加 using 引用。
using ContosoUniversity.ViewModels;
脚手架生成的代码仅仅对 OfficeAssignment 导航属性使用饿汉加载模式。
public ViewResult Index() { var instructors = db.Instructors.Include(i => i.OfficeAssignment); return View(instructors.ToList()); }
使用下面的代码替换原有的 Index 方法,读取关联的数据,通过 ViewModel 来保存。
public ActionResult Index(Int32? id, Int32? 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 和选中的课程,然后将所有需要的数据传递给视图。查询串通过页面上的 Select 超级链接提供。
代码首先创建 ViewModel 的实例,然后将教师实体列表保存在其中。
var viewModel = new InstructorIndexData(); viewModel.Instructors = db.Instructors .Include(i => i.OfficeAssignment); .Include(i => i.Courses.Select(c => c.Department)) .OrderBy(i => i.LastName);
代码使用饿汉模式加载 Instructor.OfficeAssignment 和 Instructor.Courses 导航属性。对于关联的 Course 实体,通过在 Inclue 中使用 Select 方法饿汉模式加载,结果使用 LastName 进行排序。
如果某个教师被选中了,选中的教师从 ViewModel 中的教师列表中被选出。视图模型的 Courses 属性通过教师的 Courses 属性加载相关的课程 Course 实体。
if (id != null) { ViewBag.InstructorID = id.Value; viewModel.Courses = viewModel.Instructors.Where(i => i.InstructorID == id.Value).Single().Courses; }
Where 方法返回一个集合,但是这里的情况将仅仅返回一个教师实体,Single 方法将集合转化成一个单个的实体,以便访问这个实体的 Course 属性。
在你知道集合中仅仅包含一个实体的时候,可以使用 Single 方法。Single 方法在集合中为空的时候将会抛出异常,或者在集合中包含多于一个实体的时候也会抛出异常。另外一个替换的方法是 SingleOrDefault 方法,在集合为空的时候,这个方法返回 null。实际上,在这里还是会抛出异常 ( 试图在空引用上访问 Courses 属性的时候 ),异常的信息将会简单地说明这个问题,在调用 Single 方法的时候,还可以传递一个条件来代替通过 Where 传递的条件。
.Single(i => i.InstructorID == id.Value)
替换掉:
.Where(I => i.InstructorID == id.Value).Single()
下一步,如何选中了一个课程 Course,选中的课程从视图模型 ViewModel 的 Courses 属性中获取,然后,模型的 Enrollments 属性通过课程对象的 Enrollments 导航属性被加载。
if (courseID != null) { ViewBag.CourseID = courseID.Value; viewModel.Enrollments = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments; }
最后,模型被传递到视图。
return View(viewModel);
打开 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> @String.Format("{0:d}", item.HireDate) </td> <td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td> </tr> } </table>
我们对原有的代码做了如下的变动:
<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "selectedrow"; } <tr class="@selectedRow" valign="top">
运行页面,查看教师列表,页面上显示了教师相关的 OfficeAssignment 导航属性的 Location 属性值,如果没有相关的办公室则显示为空。
如果 Views\Instructor\Index.cshtml 文件还打开,在 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> }
代码读取 ViewModel 的 Courses 属性来显示课程列表。同时还提供了 Select 链接用来发送选中的课程 Id 给 Index 方法。
运行页面,选中一个教师,现在可以显示这个教师的课程列表,可以看到每个课程所属的系。
注意,如果选中的行没有被高亮显示,刷新一下浏览器,可能需要重新加载页面相关的样式表文件。
在刚刚增加的代码块之后,增加如下的代码,用来显示注册到选中课程的学生列表。
@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 属性来显示注册到课程的学生列表,DisplayFor 方法住手方法用来是的在成绩为 null 的时候显示 “No grade”,如在这个属性的 DisplayFormat 特性中定义的那样。
运行页面,选中教师,然后选中一个课程来查看注册课程的学生和他们的成绩。
打开InstructorController.cs 文件,查看Index 方法如何获取注册学生的列表。
if (courseID != null) { ViewBag.CourseID = courseID.Value; viewModel.Enrollments = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments; }
在获取教师列表的时候,使用饿汉模式加载 Courses 导航属性值,以及 Department 导航属性的值。然后将结果保存到视图模型的 Courses 集合中,再从这个集合的一个实体中访问注册实体。因为没有对Course.Enrollements 属性指定饿汉加载,出现在页面上时将使用延迟加载。
如果仅仅禁用延迟加载而不采取其他的措施,Enrollments 属性将是 null ,而不管实际上有多少注册。在这种情况下,就必须要么指定饿汉加载,要么指定显式加载。你已经见到了如何使用饿汉加载,因为展示如何使用显式加载,将 Index 方法中替换为如下的代码,这里使用显式加载来读取 Enrollments 属性。
public ActionResult Index(Int32? id, Int32? 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 方法。再次运行程序,显示的页面并没有什么不同,虽然已经修改了获取数据的方式。
现在,你已经使用了三种加载方式 ( 延迟,饿汉,显式 )来加载导航属性相关的数据,下一次,我们将学习如何更新相关的数据。