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

在上一個教學課程中,您已完成學校資料模型。 在本教學課程中,您將讀取並顯示相關資料,也就是 Entity Framework 載入流覽屬性的資料。

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

顯示 [課程] 頁面的螢幕擷取畫面,其中包含課程清單。

Instructors_index_page_with_instructor_and_course_selected

下載已完成的專案

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

在本教學課程中,您:

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

必要條件

Entity Framework 有數種方式可以將相關資料載入實體的導覽屬性:

  • 延遲載入。 第一次讀取實體時,不會擷取相關資料。 不過,第一次嘗試存取導覽屬性時,將會自動擷取該導覽屬性所需的資料。 這會導致多個查詢傳送至資料庫— 一個用於實體本身,每次必須擷取實體的相關資料時, 一個查詢。 類別 DbContext 預設會啟用延遲載入。

    Lazy_loading_example

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

    Eager_loading_example

  • 明確載入。 這類似于延遲載入,不同之處在于您明確擷取程式碼中的相關資料;當您存取導覽屬性時,它不會自動發生。 您可以取得實體的物件狀態管理員專案,並針對集合呼叫 Collection.Load 方法,或針對保存單一實體的屬性呼叫 Reference.Load 方法,手動載入相關資料。 (在下列範例中,如果您想要載入 Administrator 導覽屬性,請將 取代 Collection(x => x.Courses)Reference(x => x.Administrator) 為 .) 通常只有在關閉延遲載入時,才會使用明確載入。

    Explicit_loading_example

因為它們不會立即擷取屬性值,所以延遲載入和明確 載入也稱為延遲載入

效能考量

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

另一方面,在某些情況下,延遲載入更有效率。 積極式載入可能會導致產生非常複雜的聯結,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 關鍵字。

  • 針對所有導覽屬性,請將 LazyLoadingEnabledfalse 下列程式碼放在內容類別別的建構函式中:

    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。 同樣地,不是具有sCoursesController。 當您選取 [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 資料。 InstructorOfficeAssignment 實體具有一對零或一關聯性。 您將針對 OfficeAssignment 實體使用積極式載入。 如上所述,當您需要主要資料表中所有擷取資料列的相關資料時,積極式載入通常更有效率。 在此情況下,您可能想要顯示所有已呈現講師的辦公室指派。
  • 當使用者選取講師時,將會顯示相關的 Course 實體。 InstructorCourse 實體具有多對多關聯性。 您將針對 Course 實體及其相關 Department 實體使用積極式載入。 在此情況下,延遲載入可能會更有效率,因為您只需要所選講師的課程。 不過,這個範例會示範如何在本身處於導覽屬性內的實體中,針對導覽屬性使用積極式載入。
  • 當使用者選取課程時,會顯示實體集中的相關資料 EnrollmentsCourseEnrollment 實體具有一對多關聯性。 您將新增 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。 同樣地,不是 InstructorsControllers。 當您選取 [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 頁面

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