處理實體關係

下載已完成的專案

本節說明 EF 如何載入相關實體,以及如何在模型類別中處理循環導覽屬性的一些詳細資料。 (本節提供背景知識,無需完成本教學課程。如果您想,跳至第 5 部分。)

預先載入與延遲載入

搭配關聯式資料庫使用 EF 時,請務必瞭解 EF 如何載入相關資料。

查看 EF 產生的 SQL 查詢也很實用。 若要追蹤 SQL,請將以下程式碼新增至 BookServiceContext 建構函式中:

public BookServiceContext() : base("name=BookServiceContext")
{
    // New code:
    this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}

如果您將 GET 要求傳送至 /api/books,它會傳回 JSON,如下所示:

[
  {
    "BookId": 1,
    "Title": "Pride and Prejudice",
    "Year": 1813,
    "Price": 9.99,
    "Genre": "Comedy of manners",
    "AuthorId": 1,
    "Author": null
  },
  ...

即使書籍包含有效的 AuthorId,您仍然可以看到作者屬性為空。 這是因為 EF 並未載入相關的作者實體。 SQL 查詢的追蹤記錄會確認此情況:

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId]
    FROM [dbo].[Books] AS [Extent1]

SELECT 陳述式取自 Books 資料表,而且不會參考 Author 資料表。

如需參考,以下是 BooksController 類別中傳回書籍清單的方法。

public IQueryable<Book> GetBooks()
{
    return db.Books;
}

讓我們來看看如何在 JSON 資料中回傳作者的資訊。 在 Entity Framework 中載入相關資料的方法有三種:積極式載入、消極式載入和明確載入。 每個技術都有所取捨,因此請務必瞭解其運作方式。

預先載入

使用積極式載入,EF 會在初始資料庫查詢中載入相關的實體。 若要執行預先載入,請使用 System.Data.Entity.Include 擴充方法。

public IQueryable<Book> GetBooks()
{
    return db.Books
        // new code:
        .Include(b => b.Author);
}

這會告訴 EF 在查詢中包含 Author 資料。 如果您進行這項變更並執行應用程式,現在 JSON 資料看起來會像這樣:

[
  {
    "BookId": 1,
    "Title": "Pride and Prejudice",
    "Year": 1813,
    "Price": 9.99,
    "Genre": "Comedy of manners",
    "AuthorId": 1,
    "Author": {
      "AuthorId": 1,
      "Name": "Jane Austen"
    }
  },
  ...

追蹤記錄顯示 EF 在 Book 和 Author 資料表上執行聯結。

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent2].[AuthorId] AS [AuthorId1], 
    [Extent2].[Name] AS [Name]
    FROM  [dbo].[Books] AS [Extent1]
    INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[AuthorId]

延遲載入

使用延遲載入時,EF 會在導覽屬性被取用時,自動載入相關的實體。 若要啟用延遲載入,請將導覽屬性設為虛擬。 例如,在 Book 類別中:

public class Book
{
    // (Other properties)

    // Virtual navigation property
    public virtual Author Author { get; set; }
}

現在考慮下列程式碼:

var books = db.Books.ToList();  // Does not load authors
var author = books[0].Author;   // Loads the author for books[0]

啟用延遲載入時,存取在 books[0] 上的 Author 屬性會導致 EF 查詢資料庫以取得作者。

延遲載入需要多次資料庫存取,因為 EF 每次擷取相關實體時都會傳送查詢。 一般而言,您應針對序列化的物件停用延遲加載。 序列化程式必須讀取模型上的所有屬性,這會觸發載入相關的實體。 例如,當 EF 序列化已啟用消極式載入的書籍清單時,以下是 SQL 查詢。 您可以看到 EF 會針對這三個作者進行三個不同的查詢。

SELECT 
    [Extent1].[BookId] AS [BookId], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[Year] AS [Year], 
    [Extent1].[Price] AS [Price], 
    [Extent1].[Genre] AS [Genre], 
    [Extent1].[AuthorId] AS [AuthorId]
    FROM [dbo].[Books] AS [Extent1]

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

SELECT 
    [Extent1].[AuthorId] AS [AuthorId], 
    [Extent1].[Name] AS [Name]
    FROM [dbo].[Authors] AS [Extent1]
    WHERE [Extent1].[AuthorId] = @EntityKeyValue1

有時候您可能想要使用消極式載入。 積極式載入可能會導致 EF 產生非常複雜的聯結。 或者,您可能需要資料的特定小部分的相關實體,而延遲載入會更有效率。

避免序列化問題的其中一種方法是序列化資料傳輸物件 (DTO),而不是實體物件。 我稍後會在文章中示範此方法。

明確式載入

明確載入類似於消極式載入,不同之處在於您在程式碼中明確取得相關資料;當您存取導覽屬性時,它不會自動發生。 明確載入可讓您更充分掌控何時載入相關資料,但需要額外的程式碼。 如需有關顯式載入的詳細資訊,請參閱載入相關實體

當我定義 Book 和 Author 模型時,我在 Book 類別上為 Book-Author 關聯性定義了一個導覽屬性,但我沒有在相反方向上定義導覽屬性。

如果您將對應的導覽屬性添加到 Author 類別,會有什麼結果?

public class Author
{
    public int AuthorId { get; set; }
    [Required]
    public string Name { get; set; }

    public ICollection<Book> Books { get; set; }
}

很遺憾,當您序列化模型時,這會造成問題。 如果您載入相關資料,它會建立循環物件圖形。

顯示 Book 類別載入 Author 類別,兩者互相載入的圖表,形成一個循環物件圖譜。

當 JSON 或 XML 格式器嘗試序列化圖形時,它會擲回例外狀況。 這兩個格式器會擲回不同的例外狀況訊息。 以下是 JSON 格式器的範例:

{
  "Message": "An error has occurred.",
  "ExceptionMessage": "The 'ObjectContent`1' type failed to serialize the response body for content type 
      'application/json; charset=utf-8'.",
  "ExceptionType": "System.InvalidOperationException",
  "StackTrace": null,
  "InnerException": {
    "Message": "An error has occurred.",
    "ExceptionMessage": "Self referencing loop detected with type 'BookService.Models.Book'. 
        Path '[0].Author.Books'.",
    "ExceptionType": "Newtonsoft.Json.JsonSerializationException",
    "StackTrace": "..."
     }
}

以下是 XML 格式器:

<Error>
  <Message>An error has occurred.</Message>
  <ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type 
    'application/xml; charset=utf-8'.</ExceptionMessage>
  <ExceptionType>System.InvalidOperationException</ExceptionType>
  <StackTrace />
  <InnerException>
    <Message>An error has occurred.</Message>
    <ExceptionMessage>Object graph for type 'BookService.Models.Author' contains cycles and cannot be 
      serialized if reference tracking is disabled.</ExceptionMessage>
    <ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType>
    <StackTrace> ... </StackTrace>
  </InnerException>
</Error>

其中一個解決方案是使用 DTO,我在下一節中說明。 或者,您可以設定 JSON 和 XML 格式器來處理圖形循環。 如需更多資訊,請參見處理循環物件參考

在本教學課程中,您不需要 Author.Book 導覽屬性,因此可以省略它。