这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻译,这里是第六篇:为ASP.NET MVC应用程序读取相关数据
原文:Reading Related Data with the Entity Framework in an ASP.NET MVC Application
译文版权所有,谢绝全文转载——但您可以在您的网站上添加到该教程的链接。
在之前的教程中您已经完成了学校数据模型。在本教程中你将学习如何读取和现实相关的数据——这里指的是通过实体框架的导航属性加载的数据。
下方的截图显示了你将要完成的页面。
实体框架拥有多种将相关数据从一个实体的导航属性中进行加载的方法:
因为延迟加载和显式加载都不立即检索属性的值,所以它们也被称为推迟加载。
如果你知道你立即需要每个实体的相关数据,预先加载通常提供最佳的性能。因为单个查询发送到数据库并一次性获取数据的效率通常比在每个实体上再发出一次查询的效率更高。例如,在上面的示例中,假定每个系有十个相关的课程,预先加载会导致只有一个查询(join联合查询)往返于数据库。延迟加载和显式加载两者都将造成11个查询和往返。在高延迟的情况下,额外的查询和往返通常是不利的。
另一方面,在某些情况下使用延迟加载的效率更高。预先加载可能会导致生成SQL Server不能有效处理的非常复杂的联接查询。或者,如果您正在处理的是需要访问的某个实体的导航属性,该属性仅为实体集的一个子集,延迟加载可能比预先加载性能更好,因为预先加载会将所有的数据全部加载,即使你不需要访问它们。如果应用程序的性能是极为重要的,你最好测试并在这两种方法之间选择一种最佳的。
延迟加载可能会屏蔽一些导致性能问题的代码。例如,代码没有指定预先或显式加载但在处理大量实体并时在每次迭代中都使用了导航属性的情况下,代码的效率可能会很低(因为会有大量的数据库往返查询)。一个在开发环境下表现良好的应用程序可能会在移动到Windows Azure SQL数据库时由于增加了延迟导致延迟加载的性能下降。你应当分析并测试以确保延迟加载是否是适当的。详细信息,请参阅Demystifying Entity Framework Strategies: Loading Related Data和Using the Entity Framework to Reduce Network Latency to SQL Azure。
如果你在序列化期间启用了延迟加载,最终你可能会查询到比预期更多的数据。序列化一般会访问类的每个属性。而属性访问触发延迟加载,然后会将延迟加载的实体也进行序列化。最终有可能会导致更多的延迟加载及属性的序列化,要防止这种链式反应,请在实体序列化之前禁用延迟加载。
有一种避免序列化问题的方式是序列化数据传输对象(DTO)而不是实体对象,如Using Web API with Entity Framework教程所示。
如果您不想使用DTO,您可以禁用延迟加载并避免通过disabling proxy creation来避免代理问题。
这里有一些禁用延迟加载的方式:
this.Configuration.LazyLoadingEnabled = false;
Course实体包含一个导航属性,里面包括了分配给该课程的Department实体。若要在课程列表中显示已分配系的名称,你需要从Department实体中获取Name属性,即Course.Department导航属性。
为Course实体类型新建一个“包含视图的MVC 5控制器(使用Entity Framework)”控制器并命名为CourseController,使用在之前你创建Student控制器一样的设置,如下图所示:
打开该控制器并查看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>
你对脚手架代码作了如下更改:
请注意在系行中,脚手架代码显示系实体的Name属性使通过导航属性来加载的。
<td> @Html.DisplayFor(modelItem => item.Department.Name) </td>
运行该页面(选择课程选项卡)以查看系名称列表。
在这一节中您将创建一个控制器和使用讲师实体的视图来显示讲师页面。
此页面通过以下方式来读取和现实相关数据:
讲师页面显示了三个不同的表格,所以您将创建一个包含三个属性的视图模型,每个属性持有一个表格所需的数据。
在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; } } }
和之前CourseController控制器一样,创建一个InstructorController控制器,如下图所示:
打开Controller\InstructorController.cs并添加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值并将所有需要的数据传给视图。页面上的选择超链接将提供这些参数。
代码首先创建视图模型的实例并将讲师列表放进模型中,该代码指定在OfficeAssignment和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方法加载课程,并且为每个课程预先加载Department导航属性。
.Include(i => i.Courses.Select(c => c.Department))
如前文所述,除非是为了提高性能,预先加载不是必须的。由于视图总是需要OfficeAssgnment实体,将它们在同一个查询中进行处理使更有效的。课程实体是在当一个讲师被选择时才需要被加载的,只有页面比没有选择更经常地显示课程,预先加载才比延迟加载更好。
如果一个讲师ID被选择了,会从视图模型的列表中来检索所选择讲师。视图模型的Courses属性通过讲师的Courses导航属性来加载相关的Course实体。
if (id != null) { ViewBag.InstructorID = id.Value; viewModel.Courses = viewModel.Instructors.Where( i => i.ID == id.Value).Single().Courses; }
Where方法返回一个集合但在这里仅仅是返回一个讲师实体。Single方法将集合转换为一个讲师实体,使您能够访问该实体的Courses属性。
当您知道该集合将只包含一个元素时,您可以使用集合上的Single方法。当你在一个空集合或存有多个元素的集合上调用Single方法时将应发一个异常。另一个选择是使用SingleOrDefault,如果该集合为空,则返回一个缺省值。但在本例中使用SingleOrDefault仍将导致异常(将尝试访问Courses属性,但该属性是一个空引用)并且异常消息会说明这点。当调用Single方法时,您还可以通过传递一个Where条件而不是分别调用Where及Single方法:
.Single( i => i.ID == id.Value)
而不是
.Where( i => i.ID == id.Value).Single()
下一步,如果选择了一个课程,将从视图模型的课程列表中检索所选择的课程。然后从课程的注册导航属性中读取注册实体并加载到到视图模型的注册属性中。
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 = "Instructor"; } <h2>Instructor</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>
您对代码做了以下更改:
<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
string selectedRow = ""; if (item.ID == ViewBag.InstructorID) { selectedRow = "success"; } <tr class="@selectedRow">
运行应用程序,然后选择讲师选项卡,页面上显示了讲师的信息,以及OfficeAssignment实体不为空情况下的Location属性,如果没有相关的办公室,则什么都不显示。
在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> }
此代码用来读取视图模中的课程属性并显示。它还提供了一个Select超链接,用来将所选课程的ID发送给Index方法。
运行页面并选择一名讲师,你将看到一个表格来显示分配给该讲师的课程。
在您刚才添加的代码之后添加下列代码,用来显示选择的课程中就读的学生列表。
@if (Model.Enrollments != null) { <h3> Students Enrolled in Select 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; }
当您检索到讲师列表时,在Courses导航属性及每个课程的系属性上您指定了预先加载。然后您将课程集合放到视图模型中,现在你就可以在集合的实体中通过注册导航属性来进行访问。因为你没有指定Course.Enrollments导航属性的预先加载,该属性中的数据将使用延迟加载,只有在呈现页面时才会加载。
如果你禁用延迟加载而不更改其他的代码,则不管实际上有多少注册,Enrollments属性将是空的。在这种情况下,如果想要加载Enrollments属性,你必须指定预先加载或显式加载。你已经见到如何使用预先加载。为了展示显式加载,使用下面的代码替换原先的学生部分,我们将在Enrollments属性上使用显式加载。
if (courseID != null) { ViewBag.CourseID = courseID.Value; //延迟加载 //viewModel.Enrollments = viewModel.Courses.Where( // x => x.CourseID == courseID).Single().Enrollments; //显式加载 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; }
在选定的Course实体后,新代码使用显式加载课程的Enrollments导航属性:
db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();
然后显式加载每个Enrollment实体相关的Student实体:
db.Entry(enrollment).Reference(x => x.Student).Load();
请注意你使用Collection方法来加载集合,但对于只有一个实体的属性,使用Reference方法来加载。
现在重新运行页面,确认一切都运行正常,但实际上你已经更改了数据检索的方式。
你现在已经尝试使用延迟、预先及显式三种加载方式来将相关数据加载到导航属性中,在下一节教程中,您将学习如何更新相关的数据。
Tom Dykstra - Tom Dykstra是微软Web平台及工具团队的高级程序员,作家。
[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序读取相关数据,布布扣,bubuko.com
[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序读取相关数据
原文:http://www.cnblogs.com/Bce-/p/3715511.html