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 通過導航屬性加載的數據。
下面的截圖展示了你將好創建的頁面。
5 – 1 延遲,餓漢,以及顯式加載關聯數據
EF 有多種方式可以通過導航屬性加載關聯的數據。
- 延遲加載 Lazy Loading。當實體第一次讀取的時候,關聯的數據並不會被獲取。 實際上,當第一次你實際訪問關聯屬性的時候,被導航屬性關聯的數據才會被自動的讀取。 這可能導致多次查詢被發送到數據庫 – 一次是讀取實體本身, 對於關聯的每個實體也需要分別讀取。
- 餓漢加載 Eager Loaing。當實體加載的時候,相關聯的數據也一起被加載。典型地用在一次連接查詢返回所有需要的相關數據,通過使用 Include 方法實現餓漢加載。
- 顯式加載 Explict Loading。這種方式類似於延遲加載,除了需要在代碼中顯式獲取數據。在你訪問導航屬性的時候,不會出現自動加載。你自己手動加載關聯的數據,通過訪問對象狀態管理器來獲取實體,調用 Collection.Load 方法獲取集合,或者通過調用持有單個實體的屬性的 Reference.Load 方法。( 在下面的示例中,如果你希望加載 Administrator 導航屬性,你應該將 Collection( x=>x.Course ) 替換為 Reference( x=>x.Administrator ) 。
因為不會立即獲取關聯屬性的值,延遲加載和顯式加載又被稱為延后加載。
一般來說,如果你知道你需要每個實體的關聯屬性,餓漢加載提供了最好的性能。因為只有一次查詢被發送到數據庫,比對每個實體都要向數據庫發出一次查詢要更加有效。例如,在上面的例子中,假設每個系都有相關的課程,餓漢加載只需要一次聯合查詢就可以獲得。而使用延遲加載或者顯式加載則需要 11 次查詢。
從另外的角度來說,如果你不常訪問實體的導航屬性,或者僅僅訪問一小部分實體的導航屬性,延遲加載更加有效,因為餓漢加載會加載更多地不必要的數據。通常情況下,在關閉了延遲加載的情況下使用顯式加載。一個關閉延遲加載的場景是在進行序列化的時候,當你知道不需要所有的導航屬性數據加載。如果延遲加載啟用,所有的導航屬性將會自動加載,因為序列化會訪問所有的屬性。
數據庫上下文默認支持延遲加載,有兩種方法可以關閉延遲加載:
- 對於特定的導航屬性,在定義屬性的時候取消 virtual
- 對於所有的導航屬性,設置 LazyLoadingEnabled 為假。
延遲加載可能導致性能問題,例如,代碼中沒有指定使用餓漢加載或者顯式加載,但是在處理大量實體的時候,遍歷每個實體並訪問其導航屬性可能導致低效率 ( 因為多次訪問數據庫 ), 但是使用延遲加載不會出現問題。在代碼使用延遲加載的時候臨時禁用延遲加載可能導致出現問題。因為導航屬性為 null 而導致代碼訪問對象失敗。
5 -2 創建顯示系名稱的課程頁面
課程 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>
這段代碼對腳手架代碼做了如下的修改:
- 將標題從 Index 修改為 Course
- 將行的鏈接移到了左邊
- 在列 Number 中顯示了 CourseID 屬性的值。( 腳手架不生成主鍵,因為通常沒有字面的意義。在這里我們希望顯示這個值而已 )
- 將最后一列標題從 DepartmentId 修改為 Department ( 系實體中的系名 )
注意,腳手架代碼顯示通過導航屬性 Department 加載的系實體的 Name 屬性值。
<td> @Html.DisplayFor(modelItem => item.Department.Name) </td>
重新運行這個頁面,( 在 Contoso 大學的首頁中選擇 Courses )來顯示系名稱的列表。
5-3 創建顯示課程和注冊信息的教師頁面
在這一節中,我們創建控制器和視圖來顯示教師實體。
這個頁面使用下面的途徑來讀取和顯示關聯的數據:
- 教師列表中的辦公室分配 OfficeAssignment 實體。教師實體與辦公室分配之間是一對一或者一對零的關系,你將使用餓漢模式來加載辦公室分配實體。從前所述,餓漢模式適合於當你需要主鍵表關聯數據的時候,在這里,你需要顯示所有教師的辦公室分配。
- 當用戶選中一個教師的時候,需要顯示這個教師相關的課程實體。教師和課程之間存在多對多的關系。你將使用餓漢模式加載課程和相關的系實體。在這里,延遲加載可能更加有效,因為僅僅需要顯示選中的教師的課程,實際上,這個例子展示了如何使用餓漢模式加載導航屬性中的導航屬性。
- 當用戶選擇課程之后,相關的注冊實體 Enrollments 將會顯示出來。Course 和 Enrollment 實體存在一對多的關系。你將使用顯式加載來處理 Enrollment 實體,以及相關的學生 Student 實體。( 由於默認支持延遲加載,所以顯示加載不是必須的。這里專門演示顯式加載 )
5-3-1 創建教師頁面的視圖模型
教師頁面顯示三個不同的表。因此,需要創建一個新的視圖模型,通過三個屬性表示出來,每一個持有一張表的數據。
在 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; } } }
5-3-2 對選中的行增加一個樣式
需要通過不同的背景色來標識選中的行,為 UI 提供一種新的樣式,將下面的代碼增加到 Content/Site.css 文件中標記為 MISC 的節中,如下所示。
/* MISC ----------------------------------------------------------*/ .selectedrow { background-color: #EEEEEE; }
5-3-3 創建教師控制器和視圖
為教師實體類型創建一個控制器。使用類似前面 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);
5-3-4 修改教師 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 非空的時候顯示 item.OfficeAssignment.Location 屬性。( 這里是一對一或者一對零的關系,可能沒有關聯的 OfficeAssignment 實體 )
<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
- 對選中教師對應行的 tr 元素,通過代碼動態增加樣式 class=”selectedrow”。這里通過前面創建的樣式類對選中的行設置背景色。( 在你在表中增加多行的列時, valign 屬性非常有用 )
string selectedRow = ""; if (item.InstructorID == ViewBag.InstructorID) { selectedRow = "selectedrow"; } <tr class="@selectedRow" valign="top">
- 在其他鏈接的前面,增加了一個名為 Select 的新的 ActionLink ,用來將選中的教師 Id 傳遞到 Index 方法。
運行頁面,查看教師列表,頁面上顯示了教師相關的 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 特性中定義的那樣。
運行頁面,選中教師,然后選中一個課程來查看注冊課程的學生和他們的成績。
5-3-5 增加顯式加載
打開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 方法。再次運行程序,顯示的頁面並沒有什么不同,雖然已經修改了獲取數據的方式。
現在,你已經使用了三種加載方式 ( 延遲,餓漢,顯式 )來加載導航屬性相關的數據,下一次,我們將學習如何更新相關的數據。