[渣譯文] 使用 MVC 5 的 EF6 Code First 入門 系列:爲ASP.NET MVC應用程序讀取相關數據

這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這裏是第七篇:爲ASP.NET MVC應用程序讀取相關數據html

原文:Reading Related Data with the Entity Framework in an ASP.NET MVC Application程序員

譯文版權全部,謝絕全文轉載——但您能夠在您的網站上添加到該教程的連接。web

在以前的教程中您已經完成了學校數據模型。在本教程中你將學習如何讀取和現實相關的數據——這裏指的是經過實體框架的導航屬性加載的數據。數據庫

下方的截圖顯示了你將要完成的頁面。api

延遲、預先和現實加載相關數據

實體框架擁有多種將相關數據從一個實體的導航屬性中進行加載的方法:mvc

  • 延遲加載(Lazy Loading)。當實體第一次被讀取時,相關數據不會被獲取。可是,當你第一次嘗試存取導航屬性時,該導航屬性所需的數據會自動加載。結果會使用多個查詢發送到數據庫——一次是讀取實體自己,而後是每一個相關的實體。DbContext類默認是使用延遲加載的。
  • 預先加載(Eager Loading)。當實體讀取時當即獲取與該實體相關的數據。這一般會致使在單個鏈接查詢中檢索出全部所須要的數據。您能夠經過使用Include方法來指定預先加載。
  • 顯式加載(Explicit Loading)。有點相似於延遲加載,只是你在代碼中顯式地獲取相關數據。當您訪問一個導航屬性時,它不會自動加載。你須要經過使用實體的對象狀態管理器並調用集合上的Collection.Load方法或經過持有單個實體的屬性的Reference.Load方法來手動加載相關數據。(在下面的示例中,若是你想要加載管理員導航屬性,你須要使用Reference(x => x.Administrator)來替換Collection(x => x.Courses))

由於延遲加載和顯式加載都不當即檢索屬性的值,因此它們也被稱爲推遲加載。app

性能注意事項

若是你知道你當即須要每一個實體的相關數據,預先加載一般提供最佳的性能。由於單個查詢發送到數據庫並一次性獲取數據的效率一般比在每一個實體上再發出一次查詢的效率更高。例如,在上面的示例中,假定每一個繫有十個相關的課程,預先加載會致使只有一個查詢(join聯合查詢)往返於數據庫。延遲加載和顯式加載二者都將形成11個查詢和往返。在高延遲的狀況下,額外的查詢和往返一般是不利的。框架

另外一方面,在某些狀況下使用延遲加載的效率更高。預先加載可能會致使生成SQL Server不能有效處理的很是複雜的聯接查詢。或者,若是您正在處理的是須要訪問的某個實體的導航屬性,該屬性僅爲實體集的一個子集,延遲加載可能比預先加載性能更好,由於預先加載會將全部的數據所有加載,即便你不須要訪問它們。若是應用程序的性能是極爲重要的,你最好測試並在這兩種方法之間選擇一種最佳的。asp.net

延遲加載可能會屏蔽一些致使性能問題的代碼。例如,代碼沒有指定預先或顯式加載但在處理大量實體並時在每次迭代中都使用了導航屬性的狀況下,代碼的效率可能會很低(由於會有大量的數據庫往返查詢)。一個在開發環境下表現良好的應用程序可能會在移動到Windows Azure SQL數據庫時因爲增長了延遲致使延遲加載的性能降低。你應當分析並測試以確保延遲加載是不是適當的。詳細信息,請參閱Demystifying Entity Framework Strategies: Loading Related DataUsing the Entity Framework to Reduce Network Latency to SQL Azure函數

在序列化以前禁用延遲加載

若是你在序列化期間啓用了延遲加載,最終你可能會查詢到比預期更多的數據。序列化通常會訪問類的每一個屬性。而屬性訪問觸發延遲加載,而後會將延遲加載的實體也進行序列化。最終有可能會致使更多的延遲加載及屬性的序列化,要防止這種鏈式反應,請在實體序列化以前禁用延遲加載。

有一種避免序列化問題的方式是序列化數據傳輸對象(DTO)而不是實體對象,如Using Web API with Entity Framework教程所示。

若是您不想使用DTO,您能夠禁用延遲加載並避免經過disabling proxy creation來避免代理問題。

這裏有一些禁用延遲加載的方式:

  • 對於特定的導航屬性,省略virtual關鍵字聲明。
  • 對於全部的導航屬性,能夠設置LazyLoadingEnabled爲false,將下面的代碼放在您上下文類的構造函數中:
    this.Configuration.LazyLoadingEnabled = false;

     

建立課程頁面,顯示系名稱

Course實體包含一個導航屬性,裏面包括了分配給該課程的Department實體。若要在課程列表中顯示已分配系的名稱,你須要從Department實體中獲取Name屬性,即Course.Department導航屬性。

爲Course實體類型新建一個「包含視圖的MVC 5控制器(使用Entity Framework)」控制器並命名爲CourseController,使用在以前你建立Student控制器同樣的設置,以下圖所示:

 打開該控制器並查看Index方法:

        public ActionResult Index()
        {
            var courses = db.Courses.Include(c => c.Department);
            return View(courses.ToList());
        }

 

你看到自動生成的腳手架使用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>

 

你對腳手架代碼做了以下更改:

  • 將標題從Index改成課程。
  • 添加了一個數字行用來顯示CourseID屬性值。缺省狀況下,腳手架不會生成主鍵模板,由於一般他們對最終用戶是沒有意義的。但在本例中,咱們只是用來展現你能夠這樣將其顯示出來而已。
  • 將課程行移動到右側並修改了它的標題,腳手架正確的選擇了Department實體的Name屬性,但在本課程頁面中,列標題應當是系,而不是Name。

請注意在系行中,腳手架代碼顯示系實體的Name屬性使經過導航屬性來加載的。

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

 

運行該頁面(選擇課程選項卡)以查看系名稱列表。

建立講師頁面以顯示課程及註冊信息

在這一節中您將建立一個控制器和使用講師實體的視圖來顯示講師頁面。

 

此頁面經過如下方式來讀取和現實相關數據:

  • 講師列表顯示OfficeAssignment實體的相關數據。Instructor和OfficeAssignment實體之間是一對一或零的關係,您可使用OfficeAssignment實體額預先加載。如前所述,當你須要主表的全部關聯數據時,預先加載是更有效的。在這裏,您想要顯示全部講師的辦公室分配狀況。
  • 當用戶選擇一名講師時將顯示相關的Course實體。Instructor和Course實體之間存在多對多的關係。您也能夠在Course實體和它們相關的Department實體上使用預先加載。但在這裏,延遲加載可能更有效,由於您僅須要已選擇講師的課程信息。實際上,這裏演示瞭如何使用延遲加載來加載導航屬性之中的導航屬性。
  • 當用戶選擇一門課程時,註冊實體記中相關的數據被顯示。Course和Enrollment實體是一對多的關係。您將添加顯式加載到Enrollment實體及它們相關的Student實體。(顯式加載實際上是沒必要要的,但這裏只是演示瞭如何執行顯式加載)

 

爲講師索引視圖建立ViewModel

講師頁面顯示了三個不一樣的表格,因此您將建立一個包含三個屬性的視圖模型,每一個屬性持有一個表格所需的數據。

在ViewModels文件夾中,建立InstructorIndexData.cs並使用下面的代碼替換原來的:

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

 

建立講師控制器和視圖

和以前CourseController控制器同樣,建立一個InstructorController控制器,以下圖所示:

打開Controller\InstructorController.cs並添加ViewModels的命名空間引用:

@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值並將全部須要的數據傳給視圖。頁面上的選擇超連接將提供這些參數。

代碼首先建立視圖模型的實例並將講師列表放進模型中,該代碼指定在OfficeAssignment和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方法加載課程,而且爲每一個課程預先加載Department導航屬性。

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

 

如前文所述,除非是爲了提升性能,預先加載不是必須的。因爲視圖老是須要OfficeAssgnment實體,將它們在同一個查詢中進行處理使更有效的。課程實體是在當一個講師被選擇時才須要被加載的,只有頁面比沒有選擇更常常地顯示課程,預先加載才比延遲加載更好。

若是一個講師ID被選擇了,會從視圖模型的列表中來檢索所選擇講師。視圖模型的Courses屬性經過講師的Courses導航屬性來加載相關的Course實體。

            if (id != null)
            {
                ViewBag.InstructorID = id.Value;
                viewModel.Courses = viewModel.Instructors.Where(
                    i => i.ID == id.Value).Single().Courses;
            }

 

Where方法返回一個集合但在這裏僅僅是返回一個講師實體。Single方法將集合轉換爲一個講師實體,使您可以訪問該實體的Courses屬性。

當您知道該集合將只包含一個元素時,您可使用集合上的Single方法。當你在一個空集合或存有多個元素的集合上調用Single方法時將應發一個異常。另外一個選擇是使用SingleOrDefault,若是該集合爲空,則返回一個缺省值。但在本例中使用SingleOrDefault仍將致使異常(將嘗試訪問Courses屬性,但該屬性是一個空引用)而且異常消息會說明這點。當調用Single方法時,您還能夠經過傳遞一個Where條件而不是分別調用Where及Single方法:

.Single( i => i.ID == id.Value)

而不是

.Where( i => i.ID == id.Value).Single()

 

 下一步,若是選擇了一個課程,將從視圖模型的課程列表中檢索所選擇的課程。而後從課程的註冊導航屬性中讀取註冊實體並加載到到視圖模型的註冊屬性中。

            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 = "Instructor";
}

<h2>Instructor</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.OfficeAssignent不爲空的時候顯示Location(由於這是一個一對一或零的關係)
                <td>
                    @if (item.OfficeAssignment != null)
                    {
                        @item.OfficeAssignment.Location
                    }
                </td>
  • 經過代碼動態的添加class="success"到所選教師的tr元素。這裏經過使用Bootstrap樣式單來設置已選擇行的背景色
            string selectedRow = "";
            if (item.ID == ViewBag.InstructorID)
            {
                selectedRow = "success";
            }
            <tr class="@selectedRow">

     

  • 添加一個新的ActionLink,用來向Index方法發送所選擇的講師ID

運行應用程序,而後選擇講師選項卡,頁面上顯示了講師的信息,以及OfficeAssignment實體不爲空狀況下的Location屬性,若是沒有相關的辦公室,則什麼都不顯示。

 

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

 

 此代碼用來讀取視圖模中的課程屬性並顯示。它還提供了一個Select超連接,用來將所選課程的ID發送給Index方法。

 運行頁面並選擇一名講師,你將看到一個表格來顯示分配給該講師的課程。

 

在您剛纔添加的代碼以後添加下列代碼,用來顯示選擇的課程中就讀的學生列表。

@if (Model.Enrollments != null)
{
    <h3>
        Students Enrolled in Select 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導航屬性及每一個課程的系屬性上您指定了預先加載。而後您將課程集合放到視圖模型中,如今你就能夠在集合的實體中經過註冊導航屬性來進行訪問。由於你沒有指定Course.Enrollments導航屬性的預先加載,該屬性中的數據將使用延遲加載,只有在呈現頁面時纔會加載。

若是你禁用延遲加載而不更改其餘的代碼,則無論實際上有多少註冊,Enrollments屬性將是空的。在這種狀況下,若是想要加載Enrollments屬性,你必須指定預先加載或顯式加載。你已經見到如何使用預先加載。爲了展現顯式加載,使用下面的代碼替換原先的學生部分,咱們將在Enrollments屬性上使用顯式加載。

            if (courseID != null)
            {
                ViewBag.CourseID = courseID.Value;
                //延遲加載
                //viewModel.Enrollments = viewModel.Courses.Where(
                //    x => x.CourseID == courseID).Single().Enrollments;
                //顯式加載
                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;
            }

 

在選定的Course實體後,新代碼使用顯式加載課程的Enrollments導航屬性:

db.Entry(selectedCourse).Collection(x => x.Enrollments).Load();

 

而後顯式加載每一個Enrollment實體相關的Student實體:

db.Entry(enrollment).Reference(x => x.Student).Load();

請注意你使用Collection方法來加載集合,但對於只有一個實體的屬性,使用Reference方法來加載。

如今從新運行頁面,確認一切都運行正常,但實際上你已經更改了數據檢索的方式。

總結

你如今已經嘗試使用延遲、預先及顯式三種加載方式來將相關數據加載到導航屬性中,在下一節教程中,您將學習如何更新相關的數據。

做者信息

  Tom Dykstra - Tom Dykstra是微軟Web平臺及工具團隊的高級程序員,做家。

相關文章
相關標籤/搜索