【EntityFramework系列教程五,翻译】在ASP.NET MVC程序中借助EntityFramework读取相关数据

在前一章中你完成了复杂的学校数据模型,在本章节中你将读取并且展示这些相关数据——也就是EntityFramework加载到导航属性中的那些数据。

以下截图展示你要完成的效果:

Courses_index_page_with_department_names

Instructors_index_page_with_instructor_and_course_selected

【慢模式、饥饿模式以及显式加载相关数据】

EntityFramework有几种方式把数据加载到导航属性中:

1)慢模式:当实体第一次被读取的时候,相关的导航属性数据并未加载;当你第一次访问该导航属性的时候才使得导航属性的数据被加入。这会对数据库产生多个请求——一个是实体自身数据的加载,另外一个则是实体中那个导航属性相关数据每次的加载:

Lazy_loading_example

2)饥饿模式:

当实体被读取时,相关属性数据同时被加载;这会产生一个对数据库的请求以便加载相关全部需要的数据,你可以通过Include的方法指定此模式:

Eager_loading_example

3)显式加载:

除了显式在代码中指定获取相关数据之外,此模式和慢加载差不多;当你访问导航模式的时候数据并不会立即加载,而是通过对象状态管理器(object state manager)获取该实体,并且通过Collection.Load加载集合对象,或者是Reference.Load的方式加载包含单个实体对象的实体。——以下例子中展示了Collection如何使用,如果你想加载Administrator导航属性,那么你必须使用Reference(x => x.Administrator)代替Collection(x => x.Courses) 。

Explicit_loading_example

因为“显式加载”和“慢加载”不会立即引发对应导航属性数据的加载,因此它们也被成为“延时加载”。

通常而言,如果你知道每一个已获取的实体的导航属性对应数据,那么“饥饿模式”加载将提供最佳性能——因为一次性查询数据库请求总比多次分散的请求数据加载要有效得多;以上例子中如果每个系只有10个相关课程,饥饿模式只会产生一次数据请求——“慢加载”和“显示模式”将产生11次数据查询请求。

从另一方面看,如果你不是频繁访问该导航属性,或者仅是处理一小部分的导航属性,那么“慢加载”将是有效的。因为“饥饿模式”要把相关数据全部加载上去,而这些数据并不是都需要进行处理的;典型情况下当你关闭了“慢加载”,那么你可以使用“显式加载”——一个情况是当你明知你不需要加载全部的导航属性的情况下,在序列化时你需要关闭慢加载;此时如果慢加载是出于打开状态,那么因为序列化要访问全部的导航属性,自然这些属性都会被全部加载。

数据库上下文默认情况下是“慢加载”——有两种方式加载:

1)对特定的导航属性,声明属性的时候请不要加“virtual”。

2)要关闭全部的导航属性,请设置LazyLoadingEnabled为Off。

“慢加载”有时会导致难以察觉的性能问题:举例来说——如果代码既没有指定“显式加载”,也没有指定“饥饿加载”,那么处理大量具备多个导航属性的数据时候,使用多次循环访问导航属性将变得效率低下(因为要来回访问数据库多次),不过如果依赖慢加载方式工作,将正确地处理相关数据。临时禁用慢加载将使得你及时发现那个地方使用了慢加载——因为这时导航属性将变成null(未被赋值),同时将引发运行时错误。

【创建一个展示系名称的课程索引页】

Course实体包含了导航属性,该属性包含了该课程隶属的系;为展示系名,你应该通过Course.Department.Name来获取这个Name属性。

为Course创建一个实体类型,应用先前对Student控制器同样的设置——如下所示:

Add_Controller_dialog_box_for_Course_controller

打开“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>

你都做了如下的改变:

 

  • 把标题从Index改成了Courses。
  • 把每一行的超链接移到了左边。
  • 在“Number”下面增加了一列,用于展示CourseID(主键通常不被显示,因为它们毫无意义;不过这个例子要显示,因为它们确有作用)。
  • 把最后一列从DepartmentID改成Department(对于Department的外键名称)。

注意最后一列——自生成的Department实体展示了Name属性,那就是加载到Department实体中的导航属性:

<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>

运行此页面(选择Contoso University主页上的Courses选项卡)来查看带有系名称的列表信息:
Courses_index_page_with_department_names

【创建一个显示课程信息及教学分配的教师索引页】  

本部分你将为Instructor创建一个控制器以便显示课程索引页面。

Instructors_index_page_with_instructor_and_course_selected

本页面将用下列方法读取并展示数据:

1)课程列表从OfficeAssignment实体中获取相关数据,Instructor和OfficeAssignment之间是“一对零”或是“一对一”关系;你通过“饥饿模式”加载OfficeAssignment实体,正如之前所介绍的那样——“饥饿模式”用于加载主表中所有信息以及其相关的其它外键表信息,这样你就可以为每一个教师显示出其对应的授课信息。

2)当用户选择了某个教师,那么相关的课程信息将被呈现;Instructor和Course是“多对多”关系,因此你将用“饥饿模式”加载Course以及对应的Department信息。此情况下实际上用“慢模式”更好,因为只是针对特定的教师显示对应的特定课程而已。不过此示例为了展示如何在导航属性中使用饥饿模式,并且这些导航属性自身也在导航属性中。

3)当用户选择了一门课程,那么相关从Enrollment读取的信息就被显示出来。Course和Enrollment是“一对多”关系,因此你用显式加载的方式读取Enrollment实体集信息以及相关的Student信息(当“慢加载”启用的时候“显式加载”并不是必要的,只不过这里为了介绍如何使用它而已)。

【为课程索引页创建视图模型类】

课程索引页展现了3张不同的表,因此你将创建一个包含三个属性的视图模型类,每一个属性又包含了这些表中某一张的所有数据。

在“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; }
}
}

【为选中的行添加CSS样式】

为标记选中行,你需要不同的背景色。欲达此目的,在Context\Site.css文件中增加此标记为MISC的代码块:

/* MISC  
----------------------------------------------------------*/
.selectedrow
{
background-color: #EEEEEE;
}

【创建Instructor控制器和视图】

为Instructor创建一个控制器,并应用你先前对Student控制器的默认设置,如下所示:

Add_Controller_dialog_box_for_Instructor_controller

打开“Controllers\InstructorController.cs”并添加一行引用视图模型的命名空间:

using ContosoUniversity.ViewModels;

在Index方法中默认只为OfficeAssignment导航属性生成了饥饿模式加载方式:

public ViewResult Index()
{
var instructors = db.Instructors.Include(i => i.OfficeAssignment);
return View(instructors.ToList());
}

请用以下代码替换先前的部分,同时加载相关的其它数据,放入视图模型类中:

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,另外一个是课程的Id;并且把所有可以查询到的数据存入视图模型类中;这些请求的参数由页面上的“Select”超链接所提供。

代码先创建了视图的实体开始,并且把一系列的实体存入模型类中:

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实体对象,通过包含在Include中Select方法也为Course.Department使用了饥饿模式;结果按照姓升序排列。

如果选择了一个特定的教师,此教师将从视图模型类“教师堆”中被取出;视图模型类的“Courses”就从那个“教师”的导航属性“Courses”中全部加载。

if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(i => i.InstructorID == id.Value).Single().Courses;
}

“Where”方法照例返回一个集合,但是在此情况下只需要返回单个“教师”实体;Single方法把该集合转换成单个“教师”实体,它同样提供了你可供访问的Courses导航属性。

当你确信你只要在一堆集合中返回一个元素,那么你可以使用Single方法。不过原来那个集合如果是空,或者查询结果多于一个,那么此方法会抛出异常;一个替代方案将使用SingleOrResult——这样即便集合为空,那么返回结果也是空;即便如此,本情况下可能还是导致异常(因为试图从空引用的集合中寻找Courses)。异常信息对于此问题的描述也不是很清楚,因此当您调用Single方法时,你可以直接在Single方法中使用条件过滤,而不必先使用Where,再使用Single:

.Single(i => i.InstructorID == id.Value)

而不是:

.Where(I => i.InstructorID == id.Value).Single()

往下看,如果你选中一个课程,同样地,选中课程也将从试图模型类的课程中被挑出,模型类的Enrollments从那个课程的Enrollments导航属性中加载全部的Enrollment实体信息:

if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments;
}

最终,视图模型被传递到视图中:

return View(viewModel);

【改变Instructor索引页】

在“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>

你对代码做了如下改变:

 

  • 把页面标题从Index改成Instructors。
  • 把超链接移至左边。
  • 移除了FullName列。
  • 增加了Office列用以展示item.OfficeAssignment.Location(如果item.OfficeAssignment.Location不是空的话……因为这是“一对零”关系,可能不一定和OfficeAssignment实体相关)。
<td> 
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>

增加的代码会对选中的教师表中“tr”动态添加“CSS类”(class="selectedrow"),这将会为你选中的行应用应用先前创建的CSS类(当你对一个表动态添加多个行时,valign属性是有用的)。

string selectedRow = ""; 
if (item.InstructorID == ViewBag.InstructorID)
{
selectedRow = "selectedrow";
}
<tr class="@selectedRow" valign="top">

运行页面查看所有的教师情况——页面展示了和OfficeAssignment相关的Location属性,当没有与之相关的OfficeAssignment时会出现一个空的表单元格:

Instructors_index_page_with_nothing_selected

如果你的“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>
}

以上代码从视图模型类中读取Courses属性中的数据显示一系列课程。同时它也提供了一个Select超链接以便把选中的课程Id传输到Index方法中。

运行此页面选择一个教师,现在你可以看到该教师所任教的全部课程;每个课程你还可以看到与之对应的系名。

Instructors_index_page_with_instructor_selected

注意:如果选中的行没有高亮,请点击Refresh按钮,因为需要重新加载css文件以便使效果生效。

在你添加的代码块的后面请继续添加下列代码——当一个课程被选中时,所有选择该课程的学生将被显示出来:

@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方法把一些未赋值学分的情况显示成“No grade”——正如在那个字段上应用的DisplayFormat数据标记。

运行页面并选择一个教师,然后选择一个课程查看与之相关所有的学生以及他们获取的学分:

Instructors_index_page_with_instructor_and_course_selected

【添加“显式加载”数据方法】

打开“InstructorController.cs”,查看一下Index方法是如何获取一个已选定课程的Enrollments信息的——

if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments;
}

当你获取了一系列的教师信息,你为导航属性Course使用了“饥饿模式”加载方法,同时也为每一个课程的Department使用了此方法;然后你把Courses塞入模型类中,现在你就可以透过那堆集合中的某个实体访问其Enrollments属性了。因为你尚未对Course.Enrollments导航属性指定饥饿模式进行加载,那么从那个属性出来的数据自然默认是“慢模式“加载的。

如果你在不改变代码情况下禁用了“慢加载”,Enrollments将会成为null——无论实际上它有多少个Enrollments。因此你要不使用“饥饿模式”或是“显式模式”来加载数据——你或许已经明白如何使用饥饿模式了,那么下面你将使用“显式模式”——请用以下代码作为示例替换Index中的代码:

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();

然后显式加载了每一个选课信息中的Student实体:

db.Entry(enrollment).Reference(x => x.Student).Load();

注意Collection方法用于加载一系列实体,而对于只包含单一实体的导航属性,我们只用Reference;你现在可以运行页面,看不到有任何变化——尽管你已经改变了数据的加载方式。

现在你已经学会了使用三种数据加载模式(慢加载、饥饿加载和显式加载),下一章你将学习如何更新相关数据。

关于其它EntityFramework资源您可以本系列最后一篇末尾处找到。

posted @ 2012-04-27 17:13  Serviceboy  阅读(850)  评论(2编辑  收藏  举报