這是微軟官方教程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
譯文版權所有,謝絕全文轉載——但您可以在您的網站上添加到該教程的鏈接。
在之前的教程中您已經完成了學校數據模型。在本教程中你將學習如何讀取和現實相關的數據——這里指的是通過實體框架的導航屬性加載的數據。
下方的截圖顯示了你將要完成的頁面。
延遲、預先和現實加載相關數據
實體框架擁有多種將相關數據從一個實體的導航屬性中進行加載的方法:
- 延遲加載(Lazy Loading)。當實體第一次被讀取時,相關數據不會被獲取。但是,當你第一次嘗試存取導航屬性時,該導航屬性所需的數據會自動加載。結果會使用多個查詢發送到數據庫——一次是讀取實體本身,然后是每個相關的實體。DbContext類默認是使用延遲加載的。
- 預先加載(Eager Loading)。當實體讀取時立即獲取與該實體相關的數據。這通常會導致在單個連接查詢中檢索出所有所需要的數據。您可以通過使用Include方法來指定預先加載。
- 顯式加載(Explicit Loading)。有點類似於延遲加載,只是你在代碼中顯式地獲取相關數據。當您訪問一個導航屬性時,它不會自動加載。你需要通過使用實體的對象狀態管理器並調用集合上的Collection.Load方法或通過持有單個實體的屬性的Reference.Load方法來手動加載相關數據。(在下面的示例中,如果你想要加載管理員導航屬性,你需要使用Reference(x => x.Administrator)來替換Collection(x => x.Courses))
因為延遲加載和顯式加載都不立即檢索屬性的值,所以它們也被稱為推遲加載。
性能注意事項
如果你知道你立即需要每個實體的相關數據,預先加載通常提供最佳的性能。因為單個查詢發送到數據庫並一次性獲取數據的效率通常比在每個實體上再發出一次查詢的效率更高。例如,在上面的示例中,假定每個系有十個相關的課程,預先加載會導致只有一個查詢(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來避免代理問題。
這里有一些禁用延遲加載的方式:
- 對於特定的導航屬性,省略virtual關鍵字聲明。
- 對於所有的導航屬性,可以設置LazyLoadingEnabled為false,將下面的代碼放在您上下文類的構造函數中:
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>
你對腳手架代碼作了如下更改:
- 將標題從Index改為課程。
- 添加了一個數字行用來顯示CourseID屬性值。缺省情況下,腳手架不會生成主鍵模板,因為通常他們對最終用戶是沒有意義的。但在本例中,我們只是用來展示你可以這樣將其顯示出來而已。
- 將課程行移動到右側並修改了它的標題,腳手架正確的選擇了Department實體的Name屬性,但在本課程頁面中,列標題應當是系,而不是Name。
請注意在系行中,腳手架代碼顯示系實體的Name屬性使通過導航屬性來加載的。
<td> @Html.DisplayFor(modelItem => item.Department.Name) </td>
運行該頁面(選擇課程選項卡)以查看系名稱列表。
創建講師頁面以顯示課程及注冊信息
在這一節中您將創建一個控制器和使用講師實體的視圖來顯示講師頁面。
此頁面通過以下方式來讀取和現實相關數據:
- 講師列表顯示OfficeAssignment實體的相關數據。Instructor和OfficeAssignment實體之間是一對一或零的關系,您可以使用OfficeAssignment實體額預先加載。如前所述,當你需要主表的所有關聯數據時,預先加載是更有效的。在這里,您想要顯示所有講師的辦公室分配情況。
- 當用戶選擇一名講師時將顯示相關的Course實體。Instructor和Course實體之間存在多對多的關系。您也可以在Course實體和它們相關的Department實體上使用預先加載。但在這里,延遲加載可能更有效,因為您僅需要已選擇講師的課程信息。實際上,這里演示了如何使用延遲加載來加載導航屬性之中的導航屬性。
- 當用戶選擇一門課程時,注冊實體記中相關的數據被顯示。Course和Enrollment實體是一對多的關系。您將添加顯式加載到Enrollment實體及它們相關的Student實體。(顯式加載其實是不必要的,但這里只是演示了如何執行顯式加載)
為講師索引視圖創建ViewModel
講師頁面顯示了三個不同的表格,所以您將創建一個包含三個屬性的視圖模型,每個屬性持有一個表格所需的數據。
在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>
您對代碼做了以下更改:
- 更改視圖的模型類為InstructorIndexData
- 更改了標題
- 添加了一個辦公室列,用來當item.OfficeAssignent不為空的時候顯示Location(因為這是一個一對一或零的關系)
<td> @if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location } </td>
- 通過代碼動態的添加class="success"到所選教師的tr元素。這里通過使用Bootstrap樣式單來設置已選擇行的背景色
string selectedRow = ""; if (item.ID == ViewBag.InstructorID) { selectedRow = "success"; } <tr class="@selectedRow">
- 添加一個新的ActionLink,用來向Index方法發送所選擇的講師ID
運行應用程序,然后選擇講師選項卡,頁面上顯示了講師的信息,以及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平台及工具團隊的高級程序員,作家。