本部分描述了EF如何加載相關(guān)實體的細節(jié),并且如何在你的模型類中處理環(huán)形導航屬性。(本部分預備了背景知識,而這不是完成這個教程所必須的。你也可以跳到第五節(jié))
預加載和延遲加載的英文名稱分別是Eager Loading和Lazy Loading。
當EF與關(guān)系數(shù)據(jù)庫一同使用時,了解EF是如何加載相關(guān)數(shù)據(jù)是非常重要的。
去查看EF生成的SQL查詢也是很有幫助的。為了追蹤SQL,添加下列代碼到BookServiceContext構(gòu)造器中:
public BookServiceContext() : base("name=BookServiceContext")
{
// New code:
this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
}
如果發(fā)送一個GET請求到/api/books,它返回像下面這樣的JSON:
[
{
"BookId": 1,
"Title": "Pride and Prejudice",
"Year": 1813,
"Price": 9.99,
"Genre": "Comedy of manners",
"AuthorId": 1,
"Author": null
},
...
你能看到Author屬性是空的,即便book包含有效的AuthorId。那是因為EF沒有在加載相關(guān)的Author實體。關(guān)于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]
該SQL跟蹤在Visual Studio的Output窗口中顯示?!g者注
SELECT語句從Books表中獲取數(shù)據(jù),但并沒有引用Author表。 作為參考,這里是在BooksController類中的方法,它返回books的列表。
public IQueryable<Book> GetBooks()
{
return db.Books;
}
來看看我們?nèi)绾尾拍茏孉uthor作為返回的JSON數(shù)據(jù)的一部分。在Entity Framework中有三種方式加載相關(guān)數(shù)據(jù):預加載(eager loading)、延遲加載(lazy loading)和顯式加載(explicit loading)。我們應該在這三種技術(shù)中有所取舍,所以了解它們是如何工作的就非常重要了。
在預加載中,EF加載相關(guān)數(shù)據(jù)作為初始化數(shù)據(jù)庫查詢的一部分。為了執(zhí)行預加載,使用System.Data.Entity.Include擴展方法。
public IQueryable<Book> GetBooks()
{
return db.Books
// new code:
.Include(b => b.Author);
}
這會告訴EF將Author數(shù)據(jù)包含在查詢中。如果你做了這個改變并運行了app,現(xiàn)在JSON數(shù)據(jù)會是如下所示:
[
{
"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表中執(zhí)行了一個join操作。
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]
在延遲加載中,當實體的導航屬性是非關(guān)聯(lián)時,EF會自動加載一個相關(guān)的實體。為了使用延遲加載,使導航屬性變成虛擬的。例如,在Book類中:
public class Book
{
// (Other properties)
// Virtual navigation property
public virtual Author Author { get; set; }
}
現(xiàn)在考慮如下代碼:
var books = db.Books.ToList(); // Does not load authors
var author = books[0].Author; // Loads the author for books[0]
當延遲加載開啟時,在books[0]上訪問Author屬性會使EF為author查詢數(shù)據(jù)庫。
延遲加載需要多段數(shù)據(jù)庫操作過程,因為每次EF發(fā)送一個查詢它都會取出一次相關(guān)實體。通常,你希望為序列化的對象禁用延遲加載。序列化已經(jīng)在模型上讀取了所有可能觸發(fā)加載相關(guān)實體的屬性。例如,下面是當延遲加載開啟后EF序列化books列表時的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生成非常復雜的聯(lián)接。或者你可能需要對于小的數(shù)據(jù)集合的相關(guān)實體,延遲加載會更加有效。
避免序列化問題的一種方式是序列化數(shù)據(jù)傳輸對象(DTOs)而不是實體對象。我將會在后面的文章中展示這種實現(xiàn)。
顯式加載和延遲加載非常類似,除了你在代碼中顯式地獲取相關(guān)數(shù)據(jù);當你訪問導航屬性時它不會自動發(fā)生。顯示加載會在加載相關(guān)數(shù)據(jù)時給你更多的控制權(quán),但也需要額外的代碼。關(guān)于顯示加載的更多信息,請查看Loading Related Entities。 http://msdn.microsoft.com/en-us/data/jj574232#explicit
當我定義Book和Author模型時,我在Book類中為Book-Author關(guān)系定義了導航屬性,但我沒有在其他方向定義導航屬性。
如果你在Author類中也定義相應的導航屬性會怎樣呢?
public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
public ICollection<Book> Books { get; set; }
}
不幸的是,當你在序列化模型時這會產(chǎn)生一個問題。如果你加載相關(guān)數(shù)據(jù),它會產(chǎn)生環(huán)形對象圖。
當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,我將會在下一節(jié)中描述它。你可以配置JSON或XML格式化程序來處理圖循環(huán)。關(guān)于更多信息,請查看Handling Circular Object References. (http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization#handling_circular_object_references)
對于本教程,你不需要Author.Book導航熟悉,所以你可以去掉它。