教學課程:讀取相關資料 - ASP.NET MVC 搭配 EF Core
在上一個教學課程中,您已完成 School 資料模型。 在此教學課程中,您將讀取並顯示相關資料-- 也就是 Entity Framework 載入到導覽屬性的資料。
下列圖例顯示了您將操作的頁面。
在本教學課程中,您已:
- 了解如何載入相關資料
- 建立 Courses 頁面
- 建立 Instructors 頁面
- 了解明確載入
必要條件
了解如何載入相關資料
Entity Framework 等物件關聯式對應 (ORM) 有幾種方式可以將相關資料載入到實體的導覽屬性:
積極式載入:讀取實體時,將會同時擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 您可以使用
Include
和ThenInclude
方法,在 Entity Framework Core 中指定積極式載入。您可以在個別查詢中擷取一些資料,而 EF 會「修正」導覽屬性。 也就是說,EF 會自動新增個別擷取的實體,其中它們屬於先前擷取之實體的導覽屬性。 對於擷取相關資料的查詢,您可以使用
Load
方法,而不是傳回清單或物件的方法,例如ToList
或Single
。明確式載入:第一次讀取實體時,不會擷取相關資料。 您可以撰寫程式碼,以擷取需要的相關資料。 如同使用個別查詢的積極式載入,明確式載入會導致多個查詢傳送至資料庫。 不同之處在於,使用明確式載入時,程式碼會指定要載入的導覽屬性。 在 Entity Framework Core 1.1 中,您可以使用
Load
方法以執行明確式載入。 例如:消極式載入:第一次讀取實體時,不會擷取相關資料。 不過,第一次嘗試存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 每當您第一次嘗試從導覽屬性取得資料時,就會傳送查詢到資料庫。 Entity Framework Core 1.0 不支援消極式載入。
效能考量
如果您知道擷取的每個實體需要相關資料,積極式載入通常可以提供最佳效能,因為傳送至資料庫的單一查詢通常比所擷取每個實體的個別查詢更有效率。 例如,假設每個部門各有十個相關課程。 所有相關資料的積極式載入會導致只有單一 (聯結) 查詢,以及資料庫的單一來回行程。 每個部門課程的個別查詢則會導致資料庫的十一個來回行程。 當延遲很高時,資料庫的額外來回行程對效能特別不利。
相反地,在某些案例中,個別查詢更有效率。 在單一查詢中進行所有相關資料的積極式載入時,可能會導致產生非常複雜的聯結,SQL Server 無法有效率地進行處理。 或者,如果您只需要針對所處理之實體集的子集存取實體的導覽屬性,則執行個別查詢可能會更好;因為預先進行所有項目的積極式載入可能會擷取比您所需更多的資料。 如果效能嚴重不足,最好先測試這兩種方式的效能,才能做出最好的選擇。
建立 Courses 頁面
Course
實體包括一個導覽屬性,其中包含已指派課程之部門的 Department
實體。 若要在課程清單中顯示所指派部門的名稱,您需要從位於 Course.Department
導覽屬性的 Department
實體中取得 Name
屬性。
針對 Course
實體型別建立名為 CoursesController
的控制器,對具有檢視的 MVC 控制器,使用 Entity Framework 使用先前針對 StudentsController
使用的相同選項,如下圖所示:
開啟 CoursesController.cs
並檢查 Index
方法。 自動 Scaffolding 已使用 Include
方法,針對 Department
導覽屬性指定積極式載入。
以下列程式碼取代 Index
方法,以針對傳回 Course 實體的 IQueryable
使用更合適的名稱 (courses
而不是 schoolContext
):
public async Task<IActionResult> Index()
{
var courses = _context.Courses
.Include(c => c.Department)
.AsNoTracking();
return View(await courses.ToListAsync());
}
開啟 Views/Courses/Index.cshtml
,並以下列程式碼取代範本程式碼。 所做的變更已醒目提示:
@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>
您已對包含 Scaffold 的程式碼進行下列變更:
已將標題從索引變更為課程。
新增顯示
CourseID
屬性值的 [編號] 資料行。 主索引鍵預設不會進行 Scaffold,因為它們對終端使用者通常沒有任何意義。 不過,在此情況下主索引鍵有意義,因此您想要顯示它。變更 [部門] 資料行來顯示部門名稱。 此程式碼會顯示已載入到
Department
導覽屬性之Department
實體的Name
屬性:@Html.DisplayFor(modelItem => item.Department.Name)
執行應用程式,並選取 [Courses] 索引標籤來查看含有部門名稱的清單。
建立 Instructors 頁面
在本節中,您將建立 Instructor
實體的控制器和檢視,以顯示 Instructors 頁面:
此頁面將以下列方式讀取和顯示相關資料:
講師清單會顯示來自
OfficeAssignment
實體的相關資料。Instructor
與OfficeAssignment
實體具有一對零或一關聯性。 您將針對OfficeAssignment
實體使用積極式載入。 如上所述,當您需要主要資料表中所有擷取資料列的相關資料時,積極式載入通常更有效率。 在此情況下,您可能想要顯示所有已呈現講師的辦公室指派。當使用者選取講師時,將會顯示相關的
Course
實體。Instructor
與Course
實體具有多對多關聯性。 您將針對Course
實體和其相關Department
實體使用積極式載入。 在此情況下,個別查詢可能更有效率,因為您只需要所選取講師的課程。 不過,這個範例會示範如何在本身處於導覽屬性內的實體中,針對導覽屬性使用積極式載入。當使用者選取課程時,將會顯示來自
Enrollments
實體集的相關資料。Course
與Enrollment
實體具有一對多關聯性。 您將針對Enrollment
實體及其相關Student
實體使用不同的查詢。
建立 Instructor [索引] 檢視的檢視模型
Instructors 頁面會顯示下列三個不同資料表的資料。 因此,您將建立包含三個屬性的檢視模型,每個保留其中一個資料表的資料。
在 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; }
}
}
建立 Instructor 控制器和檢視
使用 EF 讀取/寫入動作建立 Instructors 控制器,如下圖所示:
開啟 InstructorsController.cs
,並針對 ViewModels 命名空間新增 using 陳述式:
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
),以提供所選取講師和所選取課程的識別碼值。 這些參數由頁面上的選取超連結提供。
此程式碼是從建立檢視模型的執行個體,並將其置於講師清單開始。 這個程式碼會針對 Instructor.OfficeAssignment
和 Instructor.CourseAssignments
導覽屬性指定積極式載入。 在 CourseAssignments
屬性內載入 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();
由於檢視一律需要 OfficeAssignment
實體,因此在相同查詢中擷取該實體更有效率。 在網頁上選取講師時需要 Course 實體,因此只有在選取課程時的頁面顯示頻率高於未選取時,單一查詢才優於多個查詢。
此程式碼會重複 CourseAssignments
和 Course
,因為您需要來自 Course
的兩個屬性。 ThenInclude
呼叫的第一個字串會取得 CourseAssignment.Course
、Course.Enrollments
和 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
將用於您不需要的 Student
導覽屬性。 但呼叫 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
屬性會使用 Course
實體從該講師的 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
屬性包含 CourseAssignment
實體,您只想要來自該實體的相關 Course
實體。
當您知道集合只會有一個項目時,就可以在集合上使用 Single
方法。 如果傳遞的集合是空的或有多個項目,Single
方法會擲回例外狀況。 替代方式是 SingleOrDefault
,它會在集合是空的時傳回預設值 (在此情況下為 Null)。 不過,在此情況下仍然會造成例外狀況 (由於嘗試在 Null 參考上尋找 Courses
屬性),而例外狀況訊息會不太清楚地指出問題的原因。 當您呼叫 Single
方法時,也可以傳入 Where 條件,而不是分別呼叫 Where
方法:
.Single(i => i.ID == id.Value)
而非:
.Where(i => i.ID == id.Value).Single()
接下來,如果已選取課程,則會從檢視模型的課程清單中擷取選取的課程。 然後,檢視模型的 Enrollments
屬性會使用 Enrollment 實體從該課程的 Enrollments
導覽屬性載入。
if (courseID != null)
{
ViewData["CourseID"] = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
追蹤與不追蹤
如果要在唯讀案例中使用結果,則不追蹤的查詢很實用。 其執行速度通常較快,因為不需要設定變更追蹤資訊。 如果從資料庫擷取的實體不需要更新,則不追蹤查詢的執行效能可能會優於追蹤查詢。
在某些情況下,追蹤查詢比不追蹤查詢更有效率。 如需詳細資訊,請參閱追蹤與不追蹤查詢。
修改 Instructor [索引] 檢視
在 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 = "table-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 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>
您已對現有程式碼進行下列變更:
已將模型類別變更為
InstructorIndexData
。已將頁面標題從索引變更為講師。
新增 [辦公室] 資料行,該資料行只有在
item.OfficeAssignment
不是 Null 時才會顯示item.OfficeAssignment.Location
。 (因為這是一對零或一關聯性,所有可能沒有相關的 OfficeAssignment 實體。)@if (item.OfficeAssignment != null) { @item.OfficeAssignment.Location }
新增 [課程 資料行,以顯示每位講師所教授課程。 如需詳細資訊,請參閱 Razor 語法文章的明確的行轉換一節。
已新增程式碼,可有條件地將啟動程序 CSS 類別新增至所選講師的
tr
元素。 此類別會設定所選資料列的背景色彩。在每個資料列的其他連結之前,立即新增標示為選取的超連結,這會使得選取的講師識別碼傳送到
Index
方法。<a asp-action="Index" asp-route-id="@item.ID">Select</a> |
執行應用程式並選取 [講師] 索引標籤。沒有相關的 OfficeAssignment 實體時,此頁面就會顯示相關 OfficeAssignment 實體的 Location 屬性和空的資料表儲存格。
在 Views/Instructors/Index.cshtml
檔案的結束資料表元素 (在檔案的結尾) 之後,新增下列程式碼。 當選取講師時,此程式碼會顯示與講師相關的課程。
@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
屬性以顯示課程清單。 它也會提供選取超連結,將所選取課程的識別碼傳送至 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);
}
新的程式碼會從擷取 Instructor 實體的程式碼中,捨棄用於註冊資料的 ThenInclude
方法呼叫。 它也會捨棄 AsNoTracking
。 如果選取了講師和課程,強調顯示的程式碼就會針對所選取的課程擷取 Enrollment
實體,並針對每個 Enrollment
擷取 Student
實體。
執行應用程式,並立即移至 Instructors [索引] 頁面;雖然您已變更資料的擷取方式,但您會發現頁面上顯示的內容沒有任何差異。
取得程式碼
下一步
在本教學課程中,您已:
- 了解如何載入相關資料
- 建立 Courses 頁面
- 建立 Instructors 頁面
- 了解明確載入
若要了解如何更新相關資料,請前往下一個教學課程。