共用方式為


在 ASP.NET MVC 應用程式中使用 Entity Framework 讀取相關資料, (5/10)

By Tom Dykstra

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

注意

如果您遇到無法解決的問題, 請下載已完成的章節 ,並嘗試重現您的問題。 一般而言,您可以將程式碼與已完成的程式碼進行比較,以找出問題的解決方案。 如需一些常見的錯誤以及如何解決這些問題,請參閱 錯誤和因應措施。

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

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

顯示 Contoso 大學課程索引頁面的螢幕擷取畫面。

顯示 Contoso University Instructors Index 頁面的螢幕擷取畫面,其中已選取講師和其中一個課程。

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

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

    Lazy_loading_example

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

    Eager_loading_example

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

    Explicit_loading_example

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

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

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

一般而言,只有在關閉延遲載入時,才會使用明確載入。 您應該關閉延遲載入的其中一個案例是在序列化期間。 延遲載入和序列化不會妥善混用,如果您不小心,最後查詢的資料會比啟用延遲載入時所預期的資料還要多。 序列化通常可藉由存取類型實例上的每個屬性來運作。 屬性存取會觸發延遲載入,且這些延遲載入的實體會序列化。 序列化程式接著會存取延遲載入實體的每個屬性,這可能會導致更延遲的載入和序列化。 若要防止此離開鏈結反應,請先關閉延遲載入,再序列化實體。

資料庫內容類別別預設會執行延遲載入。 有兩種方式可以停用延遲載入:

  • 針對特定的導覽屬性,當您宣告 屬性時,請省略 virtual 關鍵字。

  • 針對所有導覽屬性,請將 設定 LazyLoadingEnabledfalse 。 例如,您可以將下列程式碼放在內容類別別的建構函式中:

    this.Configuration.LazyLoadingEnabled = false;
    

延遲載入可能會遮罩造成效能問題的程式碼。 例如,未指定積極式或明確載入的程式碼,但會處理大量實體,並在每次反復專案中使用數個導覽屬性,可能會因為許多往返資料庫) 而非常沒有效率 (。 使用內部部署 SQL Server 進行開發時,由於延遲和延遲載入增加,而移至 Azure SQL Database 時,應用程式可能會發生效能問題。 使用實際測試負載分析資料庫查詢,可協助您判斷是否適合延遲載入。 如需詳細資訊,請參閱解譯 Entity Framework 策略:載入相關資料和使用 Entity Framework 來減少SQL Azure的網路延遲

建立顯示部門名稱的課程索引頁面

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

使用您稍早針對 Student 控制器所做的相同選項,建立名為 CourseControllerCourse 的控制器, (如下圖所示,除了影像以外,您的內容類別別位於 DAL 命名空間中,而不是 models 命名空間) :

Add_Controller_dialog_box_for_Course_controller

開啟 Controllers\CourseController.cs 並查看 Index 方法:

public ViewResult 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>
    <tr>
        <th></th>
        <th>Number</th>
        <th>Title</th>
        <th>Credits</th>
        <th>Department</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <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>
        <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>
    </tr>
}
</table>

您已對包含 Scaffold 的程式碼進行下列變更:

  • 將標題從 [索引 ] 變更為 [課程]。
  • 將資料列連結移至左側。
  • 已新增標題 [數位 ] 底下的欄,顯示 CourseID 屬性值。 (根據預設,主鍵不會建立 Scaffold,因為它們通常對終端使用者沒有意義。不過,在此情況下,主鍵有意義,而且您想要顯示它。)
  • [DepartmentID ] 的最後一個資料行標題 (外鍵的名稱變更為 Department [ 部門]) 實體。

請注意,針對最後一個資料行,Scaffolded 程式碼會顯示 Name 載入 Department 至導覽屬性之 Department 實體的 屬性:

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

執行頁面 (選取 Contoso University 首頁的 [ 課程 ] 索引標籤,) 以查看具有部門名稱的清單。

Courses_index_page_with_department_names

建立顯示課程和註冊的 Instructors 索引頁面

在本節中,您將建立實體的 Instructor 控制器和檢視,以顯示 Instructors 索引頁面:

顯示 Instructors Index 頁面的螢幕擷取畫面,其中已選取講師和其中一個課程。

此頁面將以下列方式讀取和顯示相關資料:

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

建立 Instructor 索引檢視的檢視模型

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

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

為選取的資料列新增樣式

若要標記選取的資料列,您需要不同的背景色彩。 若要提供此 UI 的樣式,請將下列醒目提示的程式碼新增至Content\Site.css中的 區段 /* info and errors */ ,如下所示:

/* info and errors */
.selectedrow 
{ 
    background-color: #a4d4e6; 
}
.message-info {
    border: 1px solid;
    clear: both;
    padding: 10px 20px;
}

建立 Instructor 控制器和檢視

建立 InstructorController 控制器,如下圖所示:

Add_Controller_dialog_box_for_Instructor_controller

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

using ContosoUniversity.ViewModels;

方法中的 Index Scaffold 程式碼只會針對 OfficeAssignment 導覽屬性指定積極式載入:

public ViewResult 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.InstructorID == 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) ,以提供所選講師和選取課程的識別碼值,並將所有必要的資料傳遞至檢視。 這些參數由頁面上的選取超連結提供。

提示

路由資料

路由資料是模型系結器在路由表所指定 URL 區段中找到的資料。 例如,預設路由會 controller 指定 、 actionid 區段:

routes.MapRoute(  
 name: "Default",  
 url: "{controller}/{action}/{id}",  
 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }  
);

在下列 URL 中,預設路由會對應 InstructorcontrollerIndex 作為 action 和 1 id ;這些是路由資料值。

http://localhost:1230/Instructor/Index/1?courseID=2021

「?courseID=2021」 是查詢字串值。 如果您以查詢字串值的形式傳遞 , id 模型系結器也會運作:

http://localhost:1230/Instructor/Index?id=1&CourseID=2021

URL 是由 ActionLink Razor 檢視中的 語句所建立。 在下列程式碼中 id ,參數會比對預設路由,因此 id 會新增至路由資料。

@Html.ActionLink("Select", "Index", new { id = item.PersonID  })

在下列程式碼中, courseID 不符合預設路由中的參數,因此會新增為查詢字串。

@Html.ActionLink("Select", "Index", new { courseID = item.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.InstructorID == id.Value).Single().Courses;
}

方法 Where 會傳回集合,但在此情況下,傳遞至該方法的準則只會傳回單 Instructor 一實體。 方法 Single 會將集合轉換成單 Instructor 一實體,可讓您存取該實體的 Courses 屬性。

當您知道集合只有一個專案時,您會在集合上使用 Single 方法。 如果傳遞給它的集合是空的,或有多個專案,則 Single 方法會擲回例外狀況。 替代方法是 SingleOrDefault,如果集合是空的,則會傳回預設值 (null) 。 不過,在此情況下,仍然會導致例外狀況 (嘗試在參考) 上 null 尋找 Courses 屬性,而且例外狀況訊息較不明確指出問題的原因。 當您呼叫 方法時 Single ,也可以傳入 條件, Where 而不是個別呼叫 Where 方法:

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

不要這樣撰寫:

.Where(I => i.InstructorID == 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> 
    <tr> 
        <th></th> 
        <th>Last Name</th> 
        <th>First Name</th> 
        <th>Hire Date</th> 
        <th>Office</th>
    </tr> 
    @foreach (var item in Model.Instructors) 
    { 
        string selectedRow = ""; 
        if (item.InstructorID == ViewBag.InstructorID) 
        { 
            selectedRow = "selectedrow"; 
        } 
        <tr class="@selectedRow" valign="top"> 
            <td> 
                @Html.ActionLink("Select", "Index", new { id = item.InstructorID }) | 
                @Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) | 
                @Html.ActionLink("Details", "Details", new { id = item.InstructorID }) | 
                @Html.ActionLink("Delete", "Delete", new { id = item.InstructorID }) 
            </td> 
            <td> 
                @item.LastName 
            </td> 
            <td> 
                @item.FirstMidName 
            </td> 
            <td> 
                @Html.DisplayFor(modelItem => item.HireDate)
            </td> 
            <td> 
                @if (item.OfficeAssignment != null) 
                { 
                    @item.OfficeAssignment.Location  
                } 
            </td> 
        </tr> 
    } 
</table>

您已對現有程式碼進行下列變更:

  • 已將模型類別變更為 InstructorIndexData

  • 已將頁面標題從索引變更為講師

  • 將資料列連結資料行移至左側。

  • 已移除 FullName 資料行。

  • 新增Office 資料行,只有在 不是 Null 時 item.OfficeAssignment 才會顯示 item.OfficeAssignment.Location 。 (因為這是一對零或一關聯性,所以可能沒有相關的 OfficeAssignment 實體。)

    <td> 
        @if (item.OfficeAssignment != null) 
        { 
            @item.OfficeAssignment.Location  
        } 
    </td>
    
  • 已新增程式碼,以動態方式新增 class="selectedrow"tr 所選講師的 元素。 這會使用您稍早建立的 CSS 類別,為選取的資料列設定背景色彩。 (valign 當您將多列資料行新增至 table.) 時,下列教學課程中會很有用。

    string selectedRow = ""; 
    if (item.InstructorID == ViewBag.InstructorID) 
    { 
        selectedRow = "selectedrow"; 
    } 
    <tr class="@selectedRow" valign="top">
    
  • 新增卷 ActionLink 標為 [緊接在每個資料列的其他連結之前 選取 ],這會導致選取的講師識別碼傳送至 Index 方法。

執行應用程式,然後選取[Instructors] 索引標籤。沒有相關 OfficeAssignment 實體時,頁面會顯示 Location 相關 OfficeAssignment 實體的屬性和空白資料表單元格。

Instructors_index_page_with_nothing_selected

Views\Instructor\Index.cshtml 檔案中,在檔案結尾的結尾 table 元素 () 之後,新增下列反白顯示的程式碼。 當選取講師時,這會顯示與講師相關的課程清單。

<td> 
                @if (item.OfficeAssignment != null) 
                { 
                    @item.OfficeAssignment.Location  
                } 
            </td> 
        </tr> 
    } 
</table>

@if (Model.Courses != null) 
{ 
    <h3>Courses Taught by Selected Instructor</h3> 
<table> 
    <tr> 
        <th></th> 
        <th>ID</th> 
        <th>Title</th> 
        <th>Department</th> 
    </tr> 
 
    @foreach (var item in Model.Courses) 
    { 
        string selectedRow = ""; 
        if (item.CourseID == ViewBag.CourseID) 
        { 
            selectedRow = "selectedrow"; 
        } 
    <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 動作方法。

注意

.css檔案是由瀏覽器快取。 如果您在執行應用程式時看不到變更,請在按一下 [重新整理 ] 按鈕時 按住 CTRL 鍵,或按 CTRL+F5) ,執行硬式重新整理 (按住 CTRL 鍵。

執行頁面並選取講師。 現在您會看到一個方格,其中顯示指派給所選取講師的課程,而且在每個課程中,您可以看到指派的部門名稱。

Instructors_index_page_with_instructor_selected

在您剛才新增的程式碼區塊之後,新增下列程式碼。 這會在選取課程時,顯示已註冊該課程的學生清單。

@if (Model.Enrollments != null) 
{ 
    <h3> 
        Students Enrolled in Selected Course</h3> 
    <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 讀取檢視模型的 屬性,以顯示在課程中註冊的學生清單。

執行頁面並選取講師。 接著選取課程,以查看已註冊學生和其年級的清單。

[Instructors 索引] 頁面的螢幕擷取畫面,其中已選取講師和其中一個課程。

新增明確載入

開啟 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.InstructorID == id.Value).Single().Courses;
    }
    
    if (courseID != null)
    {
        ViewBag.CourseID = courseID.Value;
        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 資源的連結。