為ASP.NET MVC應用程式讀取相關資料
這是微軟官方教程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
在之前的教程中您已經完成了學校資料模型。在本教程中你將學習如何讀取和現實相關的資料——這裡指的是通過Entity Framework的導覽屬性載入的資料。
下方的截圖顯示了你將要完成的頁面。
延遲、預先和現實載入相關資料
Entity Framework擁有多種將相關資料從一個實體的導覽屬性中進行載入的方法: 消極式載入(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.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屬性,但該屬性是一個Null 參考)並且異常訊息會說明這點。當調用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 = "Instructors";}<h2>Instructors</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.InstructorID == ViewBag.InstructorID) { selectedRow = "success"; } <tr class="@selectedRow" valign="top"> 添加一個新的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 Selected 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屬性上使用明確式載入。
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; // Lazy loading //viewModel.Enrollments = viewModel.Courses.Where( // x => x.CourseID == courseID).Single().Enrollments; // Explicit loading var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single(); db.Entry(selectedCourse).Collection(x => x.Enrollments).Load()