教學課程:在 ASP.NET MVC 應用程式中使用 EF 讀取相關資料
在上一個教學課程中,您已完成學校資料模型。 在本教學課程中,您將讀取並顯示相關資料,也就是 Entity Framework 載入流覽屬性的資料。
下列圖例顯示了您將操作的頁面。
Contoso University 範例 Web 應用程式示範如何使用 Entity Framework 6 Code First 和 Visual Studio 建立 ASP.NET MVC 5 應用程式。 如需教學課程系列的資訊,請參閱本系列的第一個教學課程。
在本教學課程中,您:
- 了解如何載入相關資料
- 建立 Courses 頁面
- 建立 Instructors 頁面
必要條件
了解如何載入相關資料
Entity Framework 有數種方式可以將相關資料載入實體的導覽屬性:
延遲載入。 第一次讀取實體時,不會擷取相關資料。 不過,第一次嘗試存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 這會導致多個查詢傳送至資料庫— 一個用於實體本身,每次必須擷取實體的相關資料時, 一個查詢。 類別
DbContext
預設會啟用延遲載入。積極式載入。 讀取實體時,將會同時擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 您可以使用 方法來指定積極式
Include
載入。明確載入。 這類似于延遲載入,不同之處在于您明確擷取程式碼中的相關資料;當您存取導覽屬性時,它不會自動發生。 您可以取得實體的物件狀態管理員專案,並針對集合呼叫 Collection.Load 方法,或針對保存單一實體的屬性呼叫 Reference.Load 方法,手動載入相關資料。 (在下列範例中,如果您想要載入 Administrator 導覽屬性,請將 取代
Collection(x => x.Courses)
Reference(x => x.Administrator)
為 .) 通常只有在關閉延遲載入時,才會使用明確載入。
因為它們不會立即擷取屬性值,所以延遲載入和明確 載入也稱為延遲載入。
效能考量
如果您知道擷取的每個實體需要相關資料,積極式載入通常可以提供最佳效能,因為傳送至資料庫的單一查詢通常比所擷取每個實體的個別查詢更有效率。 例如,在上述範例中,假設每個部門都有十個相關課程。 積極式載入範例只會產生單一 (聯結) 查詢,以及單一往返資料庫。 延遲載入和明確載入範例會導致十一個查詢和十一次往返資料庫。 當延遲很高時,資料庫的額外來回行程對效能特別不利。
另一方面,在某些情況下,延遲載入更有效率。 積極式載入可能會導致產生非常複雜的聯結,SQL Server無法有效率地處理。 或者,如果您需要只針對一組正在處理的實體子集存取實體的導覽屬性,延遲載入可能會效能更好,因為積極式載入會擷取比您需要更多的資料。 如果效能嚴重不足,最好先測試這兩種方式的效能,才能做出最好的選擇。
延遲載入可能會遮罩造成效能問題的程式碼。 例如,未指定積極式或明確載入的程式碼,但會處理大量實體,並在每次反復專案中使用數個導覽屬性,可能會因為許多往返資料庫) 而非常沒有效率 (。 使用內部部署 SQL Server 進行開發時,由於延遲和延遲載入增加,而移至 Azure SQL Database 時,應用程式可能會發生效能問題。 使用實際測試負載分析資料庫查詢,可協助您判斷是否適合延遲載入。 如需詳細資訊,請參閱解譯 Entity Framework 策略:載入相關資料和使用 Entity Framework 來減少SQL Azure的網路延遲。
在序列化之前停用延遲載入
如果您在序列化期間保持延遲載入,最後查詢的資料會比您預期的還要多。 序列化通常可藉由存取類型實例上的每個屬性來運作。 屬性存取會觸發延遲載入,且這些延遲載入的實體會序列化。 序列化程式接著會存取延遲載入實體的每個屬性,這可能會導致更延遲的載入和序列化。 若要防止此離開鏈結反應,請先關閉延遲載入,再序列化實體。
Entity Framework 所使用的 Proxy 類別也可能很複雜,如 進階案例教學課程中所述。
避免序列化問題的其中一個方法是將資料傳輸物件序列化 (DTO) 而不是實體物件,如 搭配 Entity Framework 使用 Web API 教學課程所示。
如果您沒有使用 DTO,您可以停用延遲載入,並 藉由停用 Proxy 建立來避免 Proxy 問題。
以下是 停用延遲載入的一些其他方式:
針對特定的導覽屬性,當您宣告 屬性時,請省略
virtual
關鍵字。針對所有導覽屬性,請將
LazyLoadingEnabled
false
下列程式碼放在內容類別別的建構函式中:this.Configuration.LazyLoadingEnabled = false;
建立 Courses 頁面
實體 Course
包含導覽屬性,其中包含 Department
課程指派給的部門實體。 若要在課程清單中顯示指派的部門名稱,您需要從 Department
導覽屬性中的 Course.Department
實體取得 Name
屬性。
針對實體類型建立名為 CourseController
(不是 CoursesController 的控制器) Course
,使用與 檢視相同的 MVC 5 控制器選項,使用 您稍早針對 Student
控制器執行的 Entity Framework Scaffolder:
設定 | 值 |
---|---|
模型類別 | 選取 [課程 (ContosoUniversity.Models) 。 |
資料內容類別別 | 選取 [SchoolCoNtext (ContosoUniversity.DAL) 。 |
控制器名稱 | 輸入 CourseController。 同樣地,不是具有s的CoursesController。 當您選取 [Course (ContosoUniversity.Models) 時,系統會自動填入 控制器名稱 值。 您必須變更值。 |
保留其他預設值並新增控制器。
開啟 Controllers\CourseController.cs 並查看 Index
方法:
public ActionResult Index()
{
var courses = db.Courses.Include(c => c.Department);
return View(courses.ToList());
}
自動 Scaffolding 已使用 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>
您已對包含 Scaffold 的程式碼進行下列變更:
- 將標題從 [索引 ] 變更為 [課程]。
- 新增顯示
CourseID
屬性值的 [編號] 資料行。 根據預設,主鍵不會建立 Scaffold,因為它們通常對終端使用者沒有意義。 不過,在此情況下主索引鍵有意義,因此您想要顯示它。 - 將 [部門] 資料行移至右側,並變更其標題。 Scaffolder 已正確選擇從
Department
實體顯示Name
屬性,但在 Course 頁面中,資料行標題應該是Department而非Name。
請注意,針對 Department 資料行,Scaffolded 程式碼會顯示 Name
載入 Department
至導覽屬性之 Department
實體的 屬性:
<td>
@Html.DisplayFor(modelItem => item.Department.Name)
</td>
執行頁面 (選取 Contoso University 首頁的 [ 課程 ] 索引標籤,) 以查看具有部門名稱的清單。
建立 Instructors 頁面
在本節中,您將建立實體的 Instructor
控制器和檢視,以顯示 Instructors 頁面。 此頁面將以下列方式讀取和顯示相關資料:
- 講師清單會顯示實體的相關
OfficeAssignment
資料。Instructor
與OfficeAssignment
實體具有一對零或一關聯性。 您將針對OfficeAssignment
實體使用積極式載入。 如上所述,當您需要主要資料表中所有擷取資料列的相關資料時,積極式載入通常更有效率。 在此情況下,您可能想要顯示所有已呈現講師的辦公室指派。 - 當使用者選取講師時,將會顯示相關的
Course
實體。Instructor
與Course
實體具有多對多關聯性。 您將針對Course
實體及其相關Department
實體使用積極式載入。 在此情況下,延遲載入可能會更有效率,因為您只需要所選講師的課程。 不過,這個範例會示範如何在本身處於導覽屬性內的實體中,針對導覽屬性使用積極式載入。 - 當使用者選取課程時,會顯示實體集中的相關資料
Enrollments
。Course
與Enrollment
實體具有一對多關聯性。 您將新增Enrollment
實體及其相關Student
實體的明確載入。 因為已啟用延遲載入,所以不需要 (明確載入,但這會顯示如何執行明確載入。)
建立 Instructor 索引檢視的檢視模型
Instructors 頁面會顯示三個不同的資料表。 因此,您將建立包含三個屬性的檢視模型,每個保留其中一個資料表的資料。
在 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; }
}
}
建立 Instructor 控制器和檢視
使用 EF 讀取/寫入動作建立 InstructorController
(不是 InstructorsController) 控制器:
設定 | 值 |
---|---|
模型類別 | 選取 [Instructor (ContosoUniversity.Models) 。 |
資料內容類別別 | 選取 [SchoolCoNtext (ContosoUniversity.DAL) ]。 |
控制器名稱 | 輸入 InstructorController。 同樣地,不是 InstructorsController 與 s。 當您選取 [Course (ContosoUniversity.Models) 時,會自動填入 控制器名稱 值。 您必須變更值。 |
保留其他預設值,並新增控制器。
開啟 Controllers\InstructorController.cs ,並新增 using
命名空間的 ViewModels
語句:
using ContosoUniversity.ViewModels;
方法中的 Index
Scaffold 程式碼只會針對 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
) ,以提供所選講師和選取課程的識別碼值,並將所有必要的資料傳遞至檢視。 這些參數由頁面上的選取超連結提供。
此程式碼是從建立檢視模型的執行個體,並將其置於講師清單開始。 程式碼會指定 和 Instructor.Courses
導覽屬性的 Instructor.OfficeAssignment
積極式載入。
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
種方法會載入 Courses,並針對載入的每個 Course,它會針對 Course.Department
導覽屬性執行積極式載入。
.Include(i => i.Courses.Select(c => c.Department))
如先前所述,不需要積極式載入,而是為了改善效能而完成。 因為檢視一律需要 OfficeAssignment
實體,所以在相同的查詢中擷取它會更有效率。 Course
在網頁中選取講師時,需要實體,因此只有在頁面顯示頻率高於未選取的課程時,積極式載入會比延遲載入更好。
如果選取了講師識別碼,則會從檢視模型中的講師清單中擷取選取的講師。 然後,檢視模型的 Courses
屬性會與該講師導覽屬性中的 Courses
實體一起 Course
載入。
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(i => i.ID == id.Value).Single().Courses;
}
方法 Where
會傳回集合,但在此情況下,傳遞至該方法的準則只會傳回單 Instructor
一實體。 方法 Single
會將集合轉換成單 Instructor
一實體,可讓您存取該實體的 Courses
屬性。
當您知道集合只有一個專案時,您會在集合上使用 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)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments;
}
修改 Instructor 索引檢視
在 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
。已將頁面標題從索引變更為講師。
新增Office 資料行,只有在 不是 Null 時
item.OfficeAssignment
才會顯示item.OfficeAssignment.Location
。 (因為這是一對零或一關聯性,所以可能沒有相關的OfficeAssignment
實體。)<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
方法。
執行應用程式,然後選取[Instructors] 索引標籤。沒有相關 OfficeAssignment
實體時,頁面會顯示 Location
相關 OfficeAssignment
實體的屬性和空白資料表單元格。
在 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>
}
此程式碼會讀取檢視模型的 Courses
屬性以顯示課程清單。 它也提供 Select
超連結,將所選課程的識別碼傳送至 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;
}
當您擷取講師清單時,您指定了導覽屬性和 Department
每個課程屬性的積極式載入 Courses
。 接著,您會將 Courses
集合放在檢視模型中,現在您會從該集合中的一個實體存取 Enrollments
導覽屬性。 由於您未指定流覽屬性的積極式載入,因此因為延遲載入 Course.Enrollments
,該屬性的資料會出現在頁面中。
如果您以任何其他方式停用延遲載入而不變更程式碼,則不論課程實際擁有的註冊數目為何, Enrollments
屬性都會是 null。 在此情況下,若要載入 Enrollments
屬性,您必須指定積極式載入或明確載入。 您已經瞭解如何執行積極式載入。 若要查看明確載入的範例,請將 方法取代 Index
為下列程式碼,以明確載入 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();
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
方法。
立即執行 Instructor Index 頁面,雖然您已變更資料的擷取方式,但您不會看到頁面上顯示的內容有何差異。
取得程式碼
其他資源
您可以在 ASP.NET 資料存取 - 建議的資源中找到其他 Entity Framework 資源的連結。
後續步驟
在本教學課程中,您:
- 了解如何載入相關資料
- 建立 Courses 頁面
- 建立 Instructors 頁面
若要了解如何更新相關資料,請前往下一篇文章。
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應