接上一篇《DbContext 查詢(二)》
Eager Loading
暫且稱之為主動加載, 主動加載取決於你要告訴EF你想要加載哪些相關數據進內存,之后EF會在生成的SQL語句中使用JOIN來查詢數據。讓我們看如下示例:查詢所有Destinations以及相關的Loadings。
Example 2-24
1 private static void TestEagerLoading()
3 using ( var context = new BreakAwayContext())
4 {
5 var allDestinations = context.Destinations.Include(d => d.Lodgings);
6
7 foreach ( var destination in allDestinations)
8 {
9 Console.WriteLine(destination.Name);
10
11 foreach ( var lodging in destination.Lodgings)
12 {
13 Console.WriteLine( " - " + lodging.Name);
14 }
15 }
16 }
17 }
以上示例使用了Include方法表明查詢返回的所有Destinations應該包含了相關的Lodging數據。Include使用lambda表達式來指明那個屬性要包含在返回的數據中,大家查看MSDN會發現Include方法還有一個重載方法,接受的是一個字符串參數,在我們的示例中這樣寫Include("Lodgings"),這個重載問題重重,由於參數不是強類型的所以編譯器不能編譯時檢查正確與否,不推薦大家使用。
在單個查詢也可以包含多個相關實體數據,比如我們想查詢Lodgings並且包含PrimaryContact外加關聯的Photo。我們可以這樣下:
context.Lodgings.Include(p=>p.PrimaryContact.Photo);
再比如你想要查詢Destinations並包含Lodgings外加Lodgings相關的PrimaryContact,我們可以這么寫:
context.Destinations.Include(p=>p.Lodgings.Select(t=>t.PrimaryContact));
Include方法在同一個查詢中也可調用多次來指明加載不同的數據:
context.Lodgings
.Include(p=>p.PrimaryContact)
.Include(p=>p.SecondaryContact);
關於Eager Loading的一些缺點
有上文得知,Eager Loading在生成的SQL中使用JOIN來進行查詢,這會將以前需要多個查詢語句才能得到的結果,到現在可能只需要一個查詢語句就可以實現,但這並不總是好的。當你想要Include更多的數據,SQL語句中使用JOIN的次數也會更多,這會形成更慢以及更復雜的查詢,如果你需要相當數量的關聯數據,多個簡單查詢語句通常會比一個又大又復雜的查詢語句更快。
在LINQ查詢語句中使用Include
你也可在LINQ查詢語句中使用Include,如果你使用query syntax:
var query = from d in context.Destinations.Include(d=>d.Lodgings)
where d.Country ==""
select d;
如果你使用method syntax,則可以這樣寫:
var query = context.Destinations
.Include(d=>d.Lodgings)
.Where(d=>d.Country=="");
Include是IQueryable<T>的擴展方法, 所以在查詢的任何點都可以使用,並不需要立即就跟在DbSet之后,比如你想加載國家為澳大利亞的Destination的相關Lodgings:
var query = from d in context.Destinations
where d.Country = "Australia"
select d;
query = query.Include(d=>d.Lodgings);
記住Include並不是修改原有查詢,而是返回一個新的查詢,同時我們也強調過多次,直到有代碼訪問查詢的結果,否則EF不會執行查詢,上面的這段代碼並沒有使用查詢返回的結果,所有EF不會執行任何查詢。
Explicit Loading
第三個加載選項是Explicit Loading。Explicit Loading類似於Lazy Loading(相關聯數據是分開加載的)。當主數據被加載完,不同於Lazy Loading,Explicit Loading不會自動為你加載相關數據,你需要手動調用一個方法。
下面列出了你會使用Explicit Loading而不是Lazy Loading的一些原因:
- 你定義的類的導航字段無需再被定義為virtual的。
- 你對查詢什么時候會被發送到數據庫很清楚。Lazy Loading會潛在的生成很多的查詢,而使用Explicit Loading可以很清楚的知道查詢什么時候在哪被執行。
Explicit Loading是使用DbContext.Entry方法來實現的。Entry方法讓你可以操作DbContext內實體的所有信息。除了可以訪問存儲在實體內的當前實體的信息,還可以訪問實體的狀體以及從數據庫返回的原始實體值的信息。Entry方法還可以讓你對實體調用一些操作,包括為導航字段加載數據。
一旦我們獲取了一個實體,我們就可以使用Collection和Reference方法來查看實體導航字段的信息以及操作導航字段的方法。Load方法就是其中一個操作方法,而它的用法上篇博客都有講到,這里就不再贅述。
讓我們來用Explicit Loading來同樣的加載名稱為Grand Canyon的Destination的關聯屬性Lodgings:
Example 2-25
1 private static void TestExplicitLoading()
3 using ( var context = new BreakAwayContext())
4 {
5 var query = from d in context.Destinations
6 where d.Name == " Grand Canyon "
7 select d;
8
9 var canyon = query.Single();
10
11 context.Entry(canyon)
12 .Collection(d => d.Lodgings)
13 .Load();
14
15 Console.WriteLine( " Grand Canyon Lodging: ");
16 foreach ( var lodging in canyon.Lodgings)
17 {
18 Console.WriteLine(lodging.Name);
19 }
20 }
21 }
上面代碼中,前半部分是普通的LINQ查詢語句,之后調用了Entry方法,傳遞了canyon實例,然后調用Collection方法來操作到Lodgings導航屬性,然后加載。Collection和Reference使用lambda表達式作為參數傳遞,類似於Include方法他們同時也有字符串參數的重載方法,孰優孰劣就不再贅述了!
如果你運行上面代碼,並監控數據庫查詢語句,你會看到兩個查詢: 一個執行在代碼請求名稱為Grand Canyon的Destination的單個結果(Single)時,另一個運行在Load方法調用時。
你可以看到Explicit Loading可以使用在加載集合導航字段的所有內容上,而它也可以使用在加載一部分內容上(通過LINQ 查詢)。
Explicit Loading(顯式加載)一個導航字段(非集合)看起來非常類似,只不過調用方法變成了Reference:
var lodging = context.Lodgings.First();
context.Entry(lodging)
.Reference(p=>p.PrimaryContact)
.Load();
驗證導航字段是否被加載了
調用Reference以及Collection方法之后呢,你可以訪問IsLoaded屬性。IsLoaded會告訴你導航字段的所有內容是否從數據庫加載了。這個屬性在我們使用Lazy、Eager、Explicit Loading來加載導航字段實體的時候會被設置為True。
Example 2-26
1 private static void TestIsLoaded()
3 using ( var context = new BreakAwayContext())
4 {
5 var canyon = ( from d in context.Destinations
6 where d.Name == " Grand Canyon "
7 select d).Single();
8
9 var entry = context.Entry(canyon);
10
11 Console.WriteLine( " Before Load: {0} ", entry.Collection(d => d.Lodgings).IsLoaded);
12
13 entry.Collection(d => d.Lodgings).Load();
14 Console.WriteLine( " After Load: {0} ", entry.Collection(d => d.Lodgings).IsLoaded);
15 }
16 }
上面示例代碼運行之后一目了然:第一次打印是False,因為還沒有使用任何一種加載模式加載導航屬性實體,第二次打印就變為True了。
如果你在使用Explicit Loading,如果導航屬性內容可能已經被加載了,你就可以使用IsLoaded來決定是否要加載。
查詢集合導航屬性的內容
到現在為止你已經知道了如何加載所有集合導航屬性的內容,這樣你就可以在內存中操作數據(LINQ to Object 篩選、排序等),如果你只對集合導航屬性的一部分內容感興趣,你可以只把這部分數據加載到內存中,或者你只是想要計算數量,或別的一些計算操作,你只需要把計算結果加載到內存中。
一旦你使用了Entry和Collection方法來切入到集合導航屬性,之后你就可以使用Query方法來獲得一個LINQ查詢(導航屬性的內容)。因為這是一個LINQ查詢,之后你就能再進行篩選、排序、聚集等操作。
假設你想要找到距離最近的機場少於10英里的 名稱為Grand Canyon的Destination相關的Lodgings。你可以寫如下示例:
Example 2-27 (內存內查詢導航屬性)
1 private static void QueryLodgingDistance()
3 using ( var context = new BreakAwayContext())
4 {
5 var canyonQuery = from d in context.Destinations
6 where d.Name == " Grand Canyon "
7 select d;
8
9 var canyon = canyonQuery.Single();
15 var distanceQuery = from l in canyon.Lodgings
16 where l.MilesFromNearestAirport <= 10
17 select l;
18
19 foreach ( var lodging in distanceQuery)
20 {
21 Console.WriteLine(lodging.Name);
22 }
23 }
24 }
上面這段代碼的問題在於使用LINQ to Object來查詢Lodgings導航屬性內容。這回導致這個屬性被Lazy Loading,加載所有數據到內存中。代碼之后又對數據進行了篩選,意味着並不需要加載所有數據進內存。讓我們重寫這段代碼:Example 2-27:
1 private static void QueryLodgingDistance()
3 using ( var context = new BreakAwayContext())
4 {
5 var canyonQuery = from d in context.Destinations
6 where d.Name == " Grand Canyon "
7 select d;
8
9 var canyon = canyonQuery.Single();
10
11 var lodgingQuery = context.Entry(canyon)
12 .Collection(d => d.Lodgings)
13 .Query();
14
15 var distanceQuery = from l in lodgingQuery
16 where l.MilesFromNearestAirport <= 10
17 select l;
18
19 foreach ( var lodging in distanceQuery)
20 {
21 Console.WriteLine(lodging.Name);
22 }
23 }
24 }
更新后的這段代碼使用了Query方法來為Grand Canyon相關的Lodgings創建LINQ to Entities查詢,然后對這查詢進行篩選。下面foreach遍歷distanceQuery時EF執行SQL語句轉換並對MilesFromNearsAirport在數據庫中進行篩選。這就意味着只有你所需要的數據被加載進了內存。
也許你想知道名稱為Grand Canyon的Destinations有多少個Lodging。你可以加載所有的Lodgings然后獲得個數,但為什么不僅僅只是獲得一個單一的數字結果而無需加載所有數據呢,看如下示例:
Example 2-29
1 private static void QueryLodgingCount()
3 using ( var context = new BreakAwayContext())
4 {
5 var canyonQuery = from d in context.Destinations
6 where d.Name == " Grand Canyon "
7 select d;
8
9 var canyon = canyonQuery.Single();
10
11 var lodgingQuery = context.Entry(canyon)
12 .Collection(d => d.Lodgings)
13 .Query();
14
15 var lodgingCount = lodgingQuery.Count();
16 Console.WriteLine( " Lodging at Grand Canyon: " + lodgingCount);
17 }
18 }
以上代碼無需過多解釋,Query方法返回的是LINQ to Entities查詢,它意識到你只是需要數量然后把所有查詢推到數據庫端,所以只有一個簡單的數字從數據庫返回了
Explicit Loading 導航屬性內容的子集
你可以同時使用Query以及Load方法來進行篩選之后的顯式加載(filtered explicit load),這個explicit loading僅僅加載導航屬性內容的子集,比如你想要僅僅加載名稱為Grand Canyon的Destination的相關的Lodging以及這個相關的Lodging的名稱中包含“Hotel”的數據:
context.Entry(canyon)
.Collecction(p=>p.Lodgings)
.Query()
.Where(l=>l.Name.Contains("Hotel"))
.Load();
至此關於DbContext查詢相關的功能基本探討完了,后續博客我們繼續探討下對實體的增刪改的基本操作。