翻譯:Contoso 大學 - 5 – 讀取關聯數據


By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.

原文地址:http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/reading-related-data-with-the-entity-framework-in-an-asp-net-mvc-application

全文目錄:Contoso 大學 - 使用 EF Code First 創建 MVC 應用

在前面的課程中已經完成了 School 數據模型。在這次的課程中,將要讀取和顯示相關的數據,這里指的是 EF 通過導航屬性加載的數據。

下面的截圖展示了你將好創建的頁面。

5 – 1  延遲,餓漢,以及顯式加載關聯數據

EF 有多種方式可以通過導航屬性加載關聯的數據。

  • 延遲加載 Lazy Loading。當實體第一次讀取的時候,關聯的數據並不會被獲取。 實際上,當第一次你實際訪問關聯屬性的時候,被導航屬性關聯的數據才會被自動的讀取。 這可能導致多次查詢被發送到數據庫 – 一次是讀取實體本身, 對於關聯的每個實體也需要分別讀取。

  • 餓漢加載  Eager Loaing。當實體加載的時候,相關聯的數據也一起被加載。典型地用在一次連接查詢返回所有需要的相關數據,通過使用 Include 方法實現餓漢加載。

  • 顯式加載 Explict Loading。這種方式類似於延遲加載,除了需要在代碼中顯式獲取數據。在你訪問導航屬性的時候,不會出現自動加載。你自己手動加載關聯的數據,通過訪問對象狀態管理器來獲取實體,調用 Collection.Load 方法獲取集合,或者通過調用持有單個實體的屬性的 Reference.Load 方法。( 在下面的示例中,如果你希望加載 Administrator 導航屬性,你應該將 Collection( x=>x.Course  ) 替換為 Reference( x=>x.Administrator ) 。

因為不會立即獲取關聯屬性的值,延遲加載和顯式加載又被稱為延后加載。

一般來說,如果你知道你需要每個實體的關聯屬性,餓漢加載提供了最好的性能。因為只有一次查詢被發送到數據庫,比對每個實體都要向數據庫發出一次查詢要更加有效。例如,在上面的例子中,假設每個系都有相關的課程,餓漢加載只需要一次聯合查詢就可以獲得。而使用延遲加載或者顯式加載則需要 11 次查詢。

從另外的角度來說,如果你不常訪問實體的導航屬性,或者僅僅訪問一小部分實體的導航屬性,延遲加載更加有效,因為餓漢加載會加載更多地不必要的數據。通常情況下,在關閉了延遲加載的情況下使用顯式加載。一個關閉延遲加載的場景是在進行序列化的時候,當你知道不需要所有的導航屬性數據加載。如果延遲加載啟用,所有的導航屬性將會自動加載,因為序列化會訪問所有的屬性。

數據庫上下文默認支持延遲加載,有兩種方法可以關閉延遲加載:

  • 對於特定的導航屬性,在定義屬性的時候取消 virtual
  • 對於所有的導航屬性,設置 LazyLoadingEnabled 為假。

延遲加載可能導致性能問題,例如,代碼中沒有指定使用餓漢加載或者顯式加載,但是在處理大量實體的時候,遍歷每個實體並訪問其導航屬性可能導致低效率 ( 因為多次訪問數據庫 ), 但是使用延遲加載不會出現問題。在代碼使用延遲加載的時候臨時禁用延遲加載可能導致出現問題。因為導航屬性為 null 而導致代碼訪問對象失敗。

5 -2  創建顯示系名稱的課程頁面

課程 Course實體包含一個所屬系 Department 的導航屬性,為了顯示課程所屬系的名稱,你需要通過課程所屬的系 Department 導航屬性來獲取系的名稱 Name。

為課程實體 Course 創建一個控制器,使用與前面的學生 Student 相同的設置,如下圖所示:

打開 Controllers\CourseController.cs ,找到 Index 方法。

public ViewResult 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>
    <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>

這段代碼對腳手架代碼做了如下的修改:

  • 將標題從 Index 修改為 Course
  • 將行的鏈接移到了左邊
  • 在列 Number 中顯示了 CourseID 屬性的值。( 腳手架不生成主鍵,因為通常沒有字面的意義。在這里我們希望顯示這個值而已 )
  • 將最后一列標題從 DepartmentId 修改為 Department ( 系實體中的系名 )

注意,腳手架代碼顯示通過導航屬性 Department 加載的系實體的 Name 屬性值。

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

重新運行這個頁面,( 在 Contoso 大學的首頁中選擇 Courses )來顯示系名稱的列表。

5-3  創建顯示課程和注冊信息的教師頁面

在這一節中,我們創建控制器和視圖來顯示教師實體。

這個頁面使用下面的途徑來讀取和顯示關聯的數據:

  • 教師列表中的辦公室分配 OfficeAssignment 實體。教師實體與辦公室分配之間是一對一或者一對零的關系,你將使用餓漢模式來加載辦公室分配實體。從前所述,餓漢模式適合於當你需要主鍵表關聯數據的時候,在這里,你需要顯示所有教師的辦公室分配。
  • 當用戶選中一個教師的時候,需要顯示這個教師相關的課程實體。教師和課程之間存在多對多的關系。你將使用餓漢模式加載課程和相關的系實體。在這里,延遲加載可能更加有效,因為僅僅需要顯示選中的教師的課程,實際上,這個例子展示了如何使用餓漢模式加載導航屬性中的導航屬性。
  • 當用戶選擇課程之后,相關的注冊實體 Enrollments 將會顯示出來。Course 和 Enrollment 實體存在一對多的關系。你將使用顯式加載來處理 Enrollment 實體,以及相關的學生 Student 實體。( 由於默認支持延遲加載,所以顯示加載不是必須的。這里專門演示顯式加載 )

5-3-1  創建教師頁面的視圖模型

教師頁面顯示三個不同的表。因此,需要創建一個新的視圖模型,通過三個屬性表示出來,每一個持有一張表的數據。

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

5-3-2  對選中的行增加一個樣式

需要通過不同的背景色來標識選中的行,為 UI 提供一種新的樣式,將下面的代碼增加到 Content/Site.css 文件中標記為 MISC 的節中,如下所示。

/* MISC  
----------------------------------------------------------*/
.selectedrow 
{ 
    background-color: #EEEEEE; 
}

5-3-3  創建教師控制器和視圖

為教師實體類型創建一個控制器。使用類似前面 Student 控制器的方式創建,如下所示:

打開 Controllers\InstructorController.cs ,為 ViewModels 命名空間增加  using 引用。

using ContosoUniversity.ViewModels;

腳手架生成的代碼僅僅對 OfficeAssignment 導航屬性使用餓漢加載模式。

public ViewResult Index()
{
    var instructors = db.Instructors.Include(i => i.OfficeAssignment);
    return View(instructors.ToList());
}

使用下面的代碼替換原有的 Index 方法,讀取關聯的數據,通過 ViewModel 來保存。

public ActionResult Index(Int32? id, Int32? 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 和選中的課程,然后將所有需要的數據傳遞給視圖。查詢串通過頁面上的 Select 超級鏈接提供。

代碼首先創建 ViewModel 的實例,然后將教師實體列表保存在其中。

var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
    .Include(i => i.OfficeAssignment);
    .Include(i => i.Courses.Select(c => c.Department))
    .OrderBy(i => i.LastName);

代碼使用餓漢模式加載 Instructor.OfficeAssignment 和 Instructor.Courses 導航屬性。對於關聯的 Course 實體,通過在 Inclue 中使用 Select 方法餓漢模式加載,結果使用 LastName 進行排序。

如果某個教師被選中了,選中的教師從 ViewModel 中的教師列表中被選出。視圖模型的 Courses 屬性通過教師的 Courses 屬性加載相關的課程 Course 實體。

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

Where 方法返回一個集合,但是這里的情況將僅僅返回一個教師實體,Single 方法將集合轉化成一個單個的實體,以便訪問這個實體的 Course 屬性。

在你知道集合中僅僅包含一個實體的時候,可以使用 Single 方法。Single 方法在集合中為空的時候將會拋出異常,或者在集合中包含多於一個實體的時候也會拋出異常。另外一個替換的方法是 SingleOrDefault 方法,在集合為空的時候,這個方法返回 null。實際上,在這里還是會拋出異常 ( 試圖在空引用上訪問 Courses 屬性的時候 ),異常的信息將會簡單地說明這個問題,在調用 Single 方法的時候,還可以傳遞一個條件來代替通過 Where 傳遞的條件。

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

替換掉:

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

下一步,如何選中了一個課程 Course,選中的課程從視圖模型 ViewModel 的 Courses 屬性中獲取,然后,模型的 Enrollments 屬性通過課程對象的 Enrollments 導航屬性被加載。

if (courseID != null)
{
    ViewBag.CourseID = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments;
}

最后,模型被傳遞到視圖。

return View(viewModel);

5-3-4  修改教師 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> 
                @String.Format("{0:d}", item.HireDate) 
            </td> 
            <td> 
                @if (item.OfficeAssignment != null) 
                { 
                    @item.OfficeAssignment.Location  
                } 
            </td> 
        </tr> 
    } 
</table>

我們對原有的代碼做了如下的變動:

  • 將標題從 Index 替換成Instructors
  • 將行的鏈接移到了左邊
  • 刪除了 FullName 列
  • 增加了 Office 列,僅在 item.OfficeAssignment 非空的時候顯示 item.OfficeAssignment.Location 屬性。( 這里是一對一或者一對零的關系,可能沒有關聯的 OfficeAssignment 實體 )
<td> 
    @if (item.OfficeAssignment != null) 
    { 
        @item.OfficeAssignment.Location  
    } 
</td> 
  • 對選中教師對應行的 tr 元素,通過代碼動態增加樣式 class=”selectedrow”。這里通過前面創建的樣式類對選中的行設置背景色。( 在你在表中增加多行的列時, valign 屬性非常有用 )
string selectedRow = ""; 
if (item.InstructorID == ViewBag.InstructorID) 
{ 
    selectedRow = "selectedrow"; 
} 
<tr class="@selectedRow" valign="top"> 
  • 在其他鏈接的前面,增加了一個名為 Select 的新的 ActionLink ,用來將選中的教師 Id 傳遞到 Index 方法。

運行頁面,查看教師列表,頁面上顯示了教師相關的 OfficeAssignment 導航屬性的 Location 屬性值,如果沒有相關的辦公室則顯示為空。

如果 Views\Instructor\Index.cshtml 文件還打開,在 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> 
}

代碼讀取 ViewModel 的 Courses 屬性來顯示課程列表。同時還提供了 Select 鏈接用來發送選中的課程 Id 給 Index 方法。

運行頁面,選中一個教師,現在可以顯示這個教師的課程列表,可以看到每個課程所屬的系。

注意,如果選中的行沒有被高亮顯示,刷新一下瀏覽器,可能需要重新加載頁面相關的樣式表文件。

在剛剛增加的代碼塊之后,增加如下的代碼,用來顯示注冊到選中課程的學生列表。

@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 屬性來顯示注冊到課程的學生列表,DisplayFor 方法住手方法用來是的在成績為 null 的時候顯示 “No grade”,如在這個屬性的 DisplayFormat 特性中定義的那樣。

運行頁面,選中教師,然后選中一個課程來查看注冊課程的學生和他們的成績。

5-3-5  增加顯式加載

打開InstructorController.cs 文件,查看Index 方法如何獲取注冊學生的列表。

if (courseID != null)
{
    ViewBag.CourseID = courseID.Value;
    viewModel.Enrollments = viewModel.Courses.Where(x => x.CourseID == courseID).Single().Enrollments;
}

在獲取教師列表的時候,使用餓漢模式加載 Courses 導航屬性值,以及 Department 導航屬性的值。然后將結果保存到視圖模型的 Courses 集合中,再從這個集合的一個實體中訪問注冊實體。因為沒有對Course.Enrollements 屬性指定餓漢加載,出現在頁面上時將使用延遲加載。

如果僅僅禁用延遲加載而不采取其他的措施,Enrollments 屬性將是 null ,而不管實際上有多少注冊。在這種情況下,就必須要么指定餓漢加載,要么指定顯式加載。你已經見到了如何使用餓漢加載,因為展示如何使用顯式加載,將 Index 方法中替換為如下的代碼,這里使用顯式加載來讀取 Enrollments 屬性。

public ActionResult Index(Int32? id, Int32? 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 方法。再次運行程序,顯示的頁面並沒有什么不同,雖然已經修改了獲取數據的方式。

現在,你已經使用了三種加載方式 ( 延遲,餓漢,顯式 )來加載導航屬性相關的數據,下一次,我們將學習如何更新相關的數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM