共用方式為


處理實體關聯性

下載已完成的專案

本節說明 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
  },
  ...

您可以看到 Author 屬性為 Null,即使書籍包含有效的 AuthorId 也一樣。 這是因為 EF 未載入相關的 Author 實體。 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;
}

讓我們看看如何將 Author 作為 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-Author 關聯性的 Book 類別上定義了導覽屬性,但我沒有在其他方向定義導覽屬性。

如果您將對應的導覽屬性新增至 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 導覽屬性,因此您可以將它排除在外。