處理實體關聯性
本節說明 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; }
}
很遺憾,當您序列化模型時,這會造成問題。 如果您載入相關資料,它會建立循環物件圖形。
當 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
導覽屬性,因此您可以將它排除在外。