Contoso 大學示例 Web 應用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創建 ASP.NET Core 2.0 MVC Web 應用程序。 如欲了解更多本教程相關信息,請參閱 一、入門
在上一教程中,您完成了學校數據模型。在本章中,您將讀取和展示相關數據 -- 即,實體框架加載到導航屬性的數據。
以下圖片展示了您即將完成的頁面。
相關數據的 Eager Loading (貪婪加載), Explicit Loading (顯式加載), 和 Lazy Loading (懶加載)
ORM (對象關系映射)框架,例如說 Entity Framework, 通常有多種方式用于加載實體的導航屬性。
- Eager loading -- 貪婪加載。 當讀取實體的時候,也讀取實體相關的數據。這通常導致一個單一連接查詢,來取出所以需要的數據。在 Entity Framework 使用
Include
和ThenInclude
方法來指定貪婪加載。
image.png
您可以在分離的查詢中檢索其中一些數據, 然后讓 EF “修復” 導航屬性。 也就是說, EF 會自動將分離查詢中的實體添加到之前讀取到的實體導航屬性中。 對于檢索相關數據的查詢, 您可以使用 Load 方法代替那些返回一個list
或object
的方法,比如說ToList
或Single
。
image.png - Explicit loading -- 顯示加載。第一次讀取實體時, 相關的數據沒有被檢索。當您需要的時候,您需要寫代碼來檢索相關數據。 如同在貪婪加載中使用分離查詢一樣,顯示加載將形成多個查詢送往數據庫。不同之處在于,使用顯示加載,代碼指定的是要加載的導航屬性。在 Entity Framework 1.1 中您可以使用
Load
方法來執行顯示加載。例如:
image.png - Lazy loading -- 懶加載,或延遲加載。第一次讀取實體時, 相關的數據沒有被檢索。但是,當您嘗試訪問導航屬性時,導航屬性相關的數據將會被自動檢索出。當您首次訪問導航屬性時,將有一個查詢發往數據庫。 Entity Framework Core 1.0 不支持
Lazy loading
。
性能注意事項
如果您事先知道,對于每個實體,需要相關的數據的話,貪婪加載通常提供了最佳性能,因為發送到數據庫的一個查詢通常比多個查詢更有效率。 例如,假設每個部門有十個相關的課程,貪婪加載方法使用了一條查詢加載一個部門的所有相關數據,只需要一次數據庫往返。而對每個部門單獨查詢課程,將導致出現十一個數據庫往返。多余的數據庫往返在延遲較高時對性能特別不利。
另一方面,在某些情況下,單獨查詢會更加高效。 貪婪加載可能會導致非常復雜的聯結查詢以至于 SQL Server 無法有效處理。 或者,您只需要對一個實體集的某個子集訪問其導航屬性,單獨查詢將可能比貪婪加載表現得更好,因為貪婪加載加載了超出您需要的數據的原因。 如果性能對您非常重要的話,最好對兩種方式都進行測試來做出最佳的選擇。
創建課程頁,其中顯示部門名稱。
Course
實體包含一個導航屬性,對應課程所分配部門的 Department
實體。 若要在 Course
(課程)列表中顯示所分配 Department
(部門)的名稱,您需要從 Course.Department
導航屬性連接的 Department
實體中取得 Name
屬性。
為 Course
實體類型創建一個控制器,命名為 CoursesController,使用前面教程中用于創建 Students 控制器時用的腳手架,相同的選項 -- “視圖使用 Entity Framework 的 MVC 控制器”。如下所示:
打開
CoursesController.cs
文件,檢查 Index
方法。腳手架已自動使用 Include
方法指定 Department
導航屬性為貪婪加載。將
Index
方法替換為以下代碼, 使用一個更合適的名稱命名返回 Course
實體的 IQueryable
對象。
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}
打開 Views/Courses/Index.cshtl
,并使用以下代碼替換模板代碼。
@model IEnumerable<ContosoUniversity.Models.Course>
@{
ViewData["Title"] = "Courses";
}
<h2>Courses</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.CourseID)
</th>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Credits)
</th>
<th>
@Html.DisplayNameFor(model => model.Department)
</th>
<th></th>
</tr>
</thead>
<tbody>
@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>
<a asp-action="Edit" asp-route-id="@item.CourseID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.CourseID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.CourseID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
您對腳手架生成的代碼作了如下更改:
- 修改了課程
Index
頁面的標題 - 添加了一列用于顯示
CourseID
屬性。 默認情況下, 主鍵不會出現在腳手架代碼中,因為它們通常對最終用戶無意義。 然而, 在這兒,主鍵是有意義的,您打算顯示出來。 - 修改
Department
列以顯示部門名稱。 代碼顯示加載到Department
導航屬性中Department
實體的Name
屬性。
@Html.DisplayFor(modelItem => item.Department.Name)
運行應用程序,選擇 Course
菜單以查看含有部門名稱的列表。
創建一個教師頁面,其中顯示課程及學生注冊情況
在本節中,您將會為 Instructor
實體創建一個控制器和視圖用于展示教師。
本頁面使用以下方法讀取并展示相關數據:
- 教師列表展示來自
OfficeAssignment
實體的相關數據。Instructor
和OfficeeAssignment
實體是 一 對 零或一 關系,對OfficeAssignment
實體將使用貪婪加載方式。 如前所述, 當您需要主表所有行的相關數據時,貪婪加載是最高效的。 在這種情況下, 你希望顯示所有教師分配的辦公室。 - 當用戶選擇一個教師時,相關的課程實體將會顯示。 教師和課程實體是 “多對多” 關系。您將對課程及相關的部門實體使用貪婪加載。此時,單獨的查詢可能會更加高效,因為您只需要所選擇教師相關的課程。 不過,這個示例主要用于展示如何對導航屬性使用貪婪加載,以及對導航屬性內的實體進行貪婪加載。
- 當用戶選擇一個課程時,相關的注冊實體將會顯示。 課程和注冊實體是 “一對多” 關系。 您將會使用單獨的查詢來應對注冊實體和相關的學生實體。
創建教師索引視圖的視圖模型
教師頁顯示三個不同的表中的數據。因此,你創建的視圖模型將包括三個屬性,每個屬性包含一個表的數據。
在 SchoolViewModels 文件夾中,創建 InstructorIndexData.cs 并替換為以下代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
創建教師控制器和視圖
使用包含 EF 讀/寫 操作的控制器模板創建一個教師控制器,如下圖所示:
打開 InstructorsController.cs 和添加 Viewmodel 命名空間引用:
using ContosoUniversity.Models.SchoolViewModels;
使用以下代碼替換 Index 方法,以達到相關數據的貪婪加載,并放入視圖模型中。
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
return View(viewModel);
}
方法接受可選路由數據(id)和一個查詢字符串參數(courseID),分別對應選擇的教師和選擇的課程。參數從頁面的超鏈接中而來。
代碼首先創建一個視圖模型的實例,并在其中加入教師列表。 代碼指定對 Instrator.OfficeAssignment
和 CourseAssignments
導航屬性使用貪婪加載。 在 CourseAssignments
屬性中,Course
屬性將被加載, 然后在 Course
屬性中, Enrollments
和 Department
屬性將會被加載,同時在每個 Enrollment
實體中, Student
屬性將會被加載。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
由于視圖始終需要 OfficeAssignmet
實體,因此在同一個查詢中獲取它更有效。 當在網頁中選擇教師時,課程實體是必需的,因此只有當頁面以不是沒有選擇的課程更頻繁地顯示時,單個查詢才會比多個查詢更好。
代碼中,CourseAssignments
和 Course
重復出現,因為您需要 Course
的兩個屬性。 在第一個 ThenInclude
中獲取 CourseAssignment.Course
, Course.Enrollements
, 及 Enrollment.Student
。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
在代碼中的那一點,另一個 ThenInclude
將用于學生的導航屬性,您不需要它。 但是,調用 Include
是由 Instructor
屬性開始的,所以你必須重新遍歷整個鏈條,通過指定 Course.Department
而不是 Course.Enrollments
。
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Enrollments)
.ThenInclude(i => i.Student)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.AsNoTracking()
.OrderBy(i => i.LastName)
.ToListAsync();
在選擇了一個教師時,將執行下面的代碼。 從教師視圖模型中的列表中檢索所選的教師。 然后視圖模型的Courses
屬性和課程實體從該教師的 CourseAssignments
導航屬性中一起被加載。
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
Where
方法返回一個集合,但在本例中,傳遞給該方法的條件只會返回一個 Instructor
實體。 Single
方法將集合轉換為單個 Instructor
實體, 這樣一來,您就可以訪問該實體的 CourseAssignments
屬性。 CourseAssignments
包含 CourseAssignments
實體集合,從中得到相關的 Course
實體集。
當您知道集合將只有一個項目時,您可以在集合上使用 Single
方法。如果傳遞給它的集合為空,或者有多個項目, Single
方法會拋出異常。一個替代方法是 SingleOrDefault
,如果集合是空的,它返回一個默認值(在這種情況下為null)。 但是,在這種情況下,仍然會導致異常(嘗試在空引用上查找Courses屬性),并且異常消息將不太清楚地指出問題的原因。 當您調用 Single
方法時,您還可以傳遞 Where
條件,而無需單獨調用 Where
方法:
.Single(i => i.ID == id.Value)
而不是
.Where(I => i.ID == id.Value).Single()
接下來,如果選擇課程,則從視圖模型中的課程列表中檢索所選課程。 然后,視圖模型的 “Enrollments” 屬性將加載該課程的 “Enrollments” 導航屬性中的 “Enrollment” 實體。
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
修改 “教師索引” 視圖
在 Views/Instructors/Index.cshtml 文件中,使用以下代碼替換。
@model ContosoUniversity.Models.SchoolViewModels.InstructorIndexData
@{
ViewData["Title"] = "Instructors";
}
<h2>Instructors</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.ID == (int?)ViewData["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>
@{
foreach (var course in item.CourseAssignments)
{
@course.Course.CourseID @: @course.Course.Title <br />
}
}
</td>
<td>
<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
你對現有代碼做出以下更改:
- 修改頁面 model 類為
InstructorIndexData
。 - 修改頁面標題為
Instructors
。 - 在
item.OfficeAssignment
不為空的情況下,才添加一個Office
列,顯示item.OfficeAssignment.Location
。 (因為這是一個 "一 對 零或一" 的關系,可能沒有相關的OfficeAssignment
實體。)@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
- 添加了一個課程列,顯示每個教師所教授的課程。 請參閱 使用 @: 的顯式行轉換 有關此 Razor 語法的更多信息。
- 添加的代碼動態地將
class =“success”
添加到所選教師的tr
元素中。 這將會通過 Bootstrap 類為選擇行設置一個背景顏色。string selectedRow = ""; if (item.ID == (int?)ViewData["InstructorID"]) { selectedRow = "success"; } <tr class="@selectedRow">
- 在每行中的其他鏈接前,添加一個新的超鏈接 "Select" ,將所選教師的
ID
發送到Index
方法。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
運行應用程序,選擇 Instructor
鏈接。 當沒有相關的 OfficeAssignment
實體時,該頁面顯示相關 OfficeAssignment
實體的 Location
屬性和一個空表單元格。
在
Views/Instructors/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 == (int?)ViewData["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>
}
此代碼讀取視圖模型的 Courses
屬性以顯示課程列表。它還提供一個選擇超鏈接,將所選課程的 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
屬性,以顯示在課程中注冊的學生列表。
再次刷新頁面并選擇一個教師。 然后選擇一個課程,查看注冊學生及其成績的列表。
顯式加載
當您在 InstructorsController.cs
中檢索到教師列表時,您為 CourseAssignments
導航屬性指定了貪婪加載。
假設您期望用戶很少想要在選定的教練和課程中看到注冊。 在這種情況下,您可能只需要請求加載注冊數據。 要查看如何進行顯式加載的示例,請使用以下代碼替換 Index
方法,這將刪除 Enrollments
的貪婪加載然后顯式加載該屬性。
public async Task<IActionResult> Index(int? id, int? courseID)
{
var viewModel = new InstructorIndexData();
viewModel.Instructors = await _context.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.CourseAssignments)
.ThenInclude(i => i.Course)
.ThenInclude(i => i.Department)
.OrderBy(i => i.LastName)
.ToListAsync();
if (id != null)
{
ViewData["InstructorID"] = id.Value;
Instructor instructor = viewModel.Instructors.Where(
i => i.ID == id.Value).Single();
viewModel.Courses = instructor.CourseAssignments.Select(s => s.Course);
}
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
var selectedCourse = viewModel.Courses.Where(x => x.CourseID == courseID).Single();
await _context.Entry(selectedCourse).Collection(x => x.Enrollments).LoadAsync();
foreach (Enrollment enrollment in selectedCourse.Enrollments)
{
await _context.Entry(enrollment).Reference(x => x.Student).LoadAsync();
}
viewModel.Enrollments = selectedCourse.Enrollments;
}
return View(viewModel);
}
新代碼從用于檢索教師實體的代碼中刪除 Enrollment
數據的 ThenInclude
方法調用。如果選擇了教員和課程,高亮部分的代碼將檢索所選課程的注冊實體,以及每個注冊的學生實體。
運行應用程序,選擇 Instructor
鏈接。 可以看到,雖然您已經更改了數據的檢索方式, 頁面上顯示的內容并沒有任何區別于之前的。
小結
您現在已經使用貪婪加載,在一個查詢及多個查詢中用于讀取相關數據到導航屬性。 在下一個教程中,您將學習如何更新相關數據。