共用方式為


教學課程:在 ASP.NET MVC 應用程式中使用 EF 讀取相關資料

在上一個教學課程中,您完成了 School 資料模型。 在本教學課程中,您將讀取並顯示相關資料,即實體框架載入到導航屬性中的資料。

下列圖例顯示了您將操作的頁面。

顯示帶有課程清單的課程頁面的螢幕截圖。

Instructors_index_page_with_instructor_and_course_selected

下載已完成的項目

Contoso 大學範例 Web 應用程式示範如何使用 Entity Framework 6 Code First 和 Visual Studio 建立 ASP.NET MVC 5 應用程式。 如需教學課程系列的資訊,請參閱本系列的第一個教學課程

在本教學課程中,您已:

  • 了解如何載入相關資料
  • 建立 Courses 頁面
  • 建立 Instructors 頁面

必要條件

實體框架可以透過多種方式將相關資料載入到實體的導航屬性中:

  • 消極式載入。 第一次讀取實體時,不會擷取相關資料。 不過,第一次嘗試存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 這會導致向資料庫發送多個查詢 - 一個針對實體本身,每次必須檢索實體的相關資料時都會發送一個查詢。 DbContext 類別預設啟用延遲載入。

    Lazy_loading_example

  • 積極式載入。 讀取實體時,將會同時擷取其相關資料。 這通常會導致單一聯結查詢,其可擷取所有需要的資料。 您可以使用 Include 方法指定預先載入。

    Eager_loading_example

  • 明確式載入。 這與延遲載入類似,只不過您在程式碼中明確檢索相關資料;當您訪問導航屬性時,它不會自動發生。 您可以透過取得實體的物件狀態管理器條目並呼叫集合的 Collection.Load 方法或呼叫包含單一實體的屬性的 Reference.Load 方法來手動載入相關資料。 (在下面的範例中,如果您想要載入管理員導航屬性,則可以替換 Collection(x => x.Courses)Reference(x => x.Administrator)。) 通常,僅當您關閉延遲載入時才使用明確載入。

    Explicit_loading_example

由於它們不會立即檢索屬性值,因此 延遲載入和明確載入也稱為延遲載入

效能考量

如果您知道擷取的每個實體需要相關資料,積極式載入通常可以提供最佳效能,因為傳送至資料庫的單一查詢通常比所擷取每個實體的個別查詢更有效率。 例如,在上面的範例中,假設每個部門有十門相關課程。 急切載入範例將僅導致單一 (連接) 查詢和到資料庫的單次往返。 延遲載入和明確載入範例都會導致十一次查詢和十一次資料庫往返。 當延遲很高時,資料庫的額外來回行程對效能特別不利。

另一方面,在某些場景下,延遲載入的效率更高。 預先載入可能會導致產生非常複雜的聯接,而 SQL Server 無法有效地處理該聯結。 或者,如果您只需要存取正在處理的一組實體的子集的實體導航屬性,則延遲載入可能會執行得更好,因為預先載入會擷取比您需要的更多的資料。 如果效能嚴重不足,最好先測試這兩種方式的效能,才能做出最好的選擇。

延遲載入可以屏蔽導致效能問題的程式碼。 例如,未指定急切或明確載入但處理大量實體並在每次迭代中使用多個導航屬性的程式碼可能非常低效 (因為與資料庫的多次往返)。 使用本機 SQL 伺服器在開發中表現良好的應用程式在遷移到 Azure SQL 資料庫時可能會因延遲增加和延遲載入而出現效能問題。 使用實際測試負載分析資料庫查詢將幫助您確定延遲載入是否合適。 有關詳細信息,請參閱揭秘實體框架策略:載入相關資料使用實體框架減少 SQL Azure 的網路延遲

在序列化之前禁用延遲載入

如果在序列化期間啟用延遲加載,則最終查詢的資料可能會比預期多得多。 序列化通常透過存取類型執行個體上的每個屬性來工作。 屬性存取會觸發延遲加載,並且那些延遲加載的實體將被序列化。 然後,序列化過程存取延遲載入實體的每個屬性,這可能會導致更多的延遲載入和序列化。 為了防止這種失控的連鎖反應,請在序列化實體之前關閉延遲載入。

實體框架使用的代理程式類別也可能使序列化變得複雜,如進階場景教學課程所述。

避免序列化問題的一種方法是序列化資料傳輸物件 (DTO) 而不是實體物件,如將 Web API 與實體框架結合使用教學課程中所示。

如果您不使用 DTO,則可以停用延遲載入並透過停用代理建立來避免代理問題。

以下是禁用延遲載入的其他 一些方法:

  • 對於特定的導航屬性,在聲明屬性時省略 virtual 關鍵字。

  • 對於所有導航屬性,設定為 LazyLoadingEnabledfalse 將以下程式碼放入上下文類別的建構函式中:

    this.Configuration.LazyLoadingEnabled = false;
    

建立 Courses 頁面

Course 實體包括一個導覽屬性,其中包含已指派課程之部門的 Department 實體。 若要在課程清單中顯示指定部門的名稱,您需要從 Course.Department 導覽 Name 屬性中的 Department 實體取得該屬性。

為實體類型建立一個名為 CourseController(不是 CoursesController)CourseStudent控制器,使用與具有檢視的 MVC 5 控制器相同的選項,並使用您先前為控制器所做的實體框架鷹架:

設定
模型類 選擇課程 (ContosoUniversity.Models)
資料上下文類 選擇 SchoolContext (ContosoUniversity.DAL)
控制器名稱 輸入課程控制器。 再說一次,不是帶有 sCoursesController。 當您選擇課程 (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 屬性值的 [編號] 資料行。 預設情況下,主鍵不會被搭建起來,因為通常它們對最終用戶來說毫無意義。 不過,在此情況下主索引鍵有意義,因此您想要顯示它。
  • 部門列移至右側並更改其標題。 支架正確地選擇顯示 Department 實體的 Name 屬性,但在課程頁面中,列標題應該是部門而不是名稱

請注意,對於 Department 列,支架程式碼顯示載入到 Department 導覽屬性中的 Department 實體的 Name 屬性:

<td>
    @Html.DisplayFor(modelItem => item.Department.Name)
</td>

執行該頁面 (選擇 Contoso University 主頁上的課程標籤) 以查看包含部門名稱的清單。

建立 Instructors 頁面

在本部分中,您將為 Instructor 實體建立一個控制器和檢視,以便顯示講師頁面。 此頁面將以下列方式讀取和顯示相關資料:

  • 講師清單會顯示來自 OfficeAssignment 實體的相關資料。 InstructorOfficeAssignment 實體具有一對零或一關聯性。 您將針對 OfficeAssignment 實體使用積極式載入。 如上所述,當您需要主要資料表中所有擷取資料列的相關資料時,積極式載入通常更有效率。 在此情況下,您可能想要顯示所有已呈現講師的辦公室指派。
  • 當使用者選取講師時,將會顯示相關的 Course 實體。 InstructorCourse 實體具有多對多關聯性。 您將針對 Course 實體和其相關 Department 實體使用積極式載入。 在這種情況下,延遲載入可能會更有效,因為您只需要所選講師的課程。 不過,這個範例會示範如何在本身處於導覽屬性內的實體中,針對導覽屬性使用積極式載入。
  • 當使用者選取課程時,將會顯示來自 Enrollments 實體集的相關資料。 CourseEnrollment 實體具有一對多關聯性。 您將為 Enrollment 實體及其相關 Student 實體新增明確載入。 (明確加載不是必需的,因為啟用了延遲加載,但這顯示瞭如何進行明確加載。)

為教師索引檢視建立檢視模型

教師頁面顯示三個不同的表格。 因此,您將建立包含三個屬性的檢視模型,每個保留其中一個資料表的資料。

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; }
    }
}

建立教練控制器和檢視

建立一個 InstructorController 具有 EF 讀/寫操作的 (不是 InstructorsController) 控制器:

設定
模型類 選擇講師 (ContosoUniversity.Models)
資料上下文類 選擇 SchoolContext (ContosoUniversity.DAL)
控制器名稱 輸入 InstructorController。 再說一次,不是帶有 sInstructorsController。 當您選擇課程 (ContosoUniversity.Models) 時,控制器名稱值會自動填入。 你必須改變這個值。

保留其他預設值並新增控制器。

開啟 Controllers\InstructorController.cs 並新增 ViewModels 命名空間的 using 語句:

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 值,並將所有必需的資料傳遞到檢視。 這些參數由頁面上的選取超連結提供。

此程式碼是從建立檢視模型的執行個體,並將其置於講師清單開始。 該程式碼指定了 Instructor.OfficeAssignmentInstructor.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 方法會載入課程,並且對於載入的每個課程,它都會預先載入 Course.Department 導航屬性。

.Include(i => i.Courses.Select(c => c.Department))

如前所述,急切加載不是必需的,但這樣做是為了提高效能。 由於檢視一律需要 OfficeAssignment 實體,因此在相同查詢中擷取該實體更有效率。 在網頁中選擇講師時需要 Course 實體,因此只有在選擇課程時頁面顯示的頻率高於未選擇課程的情況下,急切加載才比延遲加載更好。

如果選擇了講師 ID,則會從檢視模型中的講師清單中檢索所選講師。 然後檢視模型的 Courses 屬性會使用 Course 實體從該講師的 Courses 導覽屬性載入。

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 屬性會載入該課程 Enrollments 導航屬性中的 Enrollment 實體。

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.OfficeAssignment 不為空白時顯示的 item.OfficeAssignment.Location Office 列。 (因為這是一對零或一的關係,因此可能不存在相關 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 標記的選擇,這會導致將選定的講師 ID 傳送到 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 超連結,將所選課程的 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 導航屬性和每個課程的 Department 屬性指定了預先載入。 然後,您將該集合放入檢視模型中,現在您可以從 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 方法。

現在運行「教師索引」頁面,儘管您更改了資料的檢索方式,但頁面上顯示的內容沒有任何差異。

取得程式碼

下載已完成的項目

其他資源

可以在 ASP.NET 資料存取 - 推薦資源中找到其他實體框架資源的連結。

下一步

在本教學課程中,您已:

  • 了解如何載入相關資料
  • 建立 Courses 頁面
  • 建立 Instructors 頁面

若要了解如何更新相關資料,請前往下一篇文章。