接上一篇《DbContext 查詢》。
對本地數據運行LINQ查詢
由上篇博客可得知,Local屬性返回的是內存中的數據集合,那使用LINQ to Object我們可以對這些數據運行查詢。
查看一下示例:Example 2-21
1 private static void LocalLinqQueries()
3 using ( var context = new BreakAwayContext())
4 {
5 context.Destinations.Load();
6
7 var sortedDestinations = from d in context.Destinations.Local
8 orderby d.Name
9 select d;
10
11 Console.WriteLine( " All Destinations: ");
12 foreach ( var destination in sortedDestinations)
13 {
14 Console.WriteLine(destination.Name);
15 }
16
17 var aussieDestinations = from d in context.Destinations.Local
18 where d.Country == " Australia "
19 select d;
20
21 Console.WriteLine();
22 Console.WriteLine( " Australian Destinations: ");
23 foreach ( var destination in aussieDestinations)
24 {
25 Console.WriteLine(destination.Name);
26 }
27 }
28 }
上面這段代碼加載所有Destinations到內存中,然后對內存中的數據運行根據名稱以及根據國家的篩選查詢,記住Find方法默認也是優先從內存中獲取數據的。
使用Load以及Local在減輕數據庫查詢方面是蠻好的,但是記住把所有的數據都加載到內存中是一個昂貴的操作,如果你運行多個查詢而僅僅是返回你數據的子集,你可以僅僅把你需要的數據加載到內存中,而不是把所有數據。
各LINQ提供程序之間的區別
針對DbSet以及Local的查詢有一些微妙但重要的區別,這兩個數據源使用兩種不同的LINQ 提供程序。DbSet使用的是LINQ to Entities:針對Entity Framework使用Model和映射來將你的查詢轉換成SQL,Local使用的是LINQ to Object:使用標准的.NET操作來對數據進行篩選排序等操作。同樣的查詢語法會返回不同的結果,取決於你使用的LINQ提供程序類型。比如:數據庫比對String類型是大小寫不敏感的,但是.NET是大小寫敏感的。這就造成同樣的查詢條件返回的結果是不一致的。
大部分的LINQ 提供程序支持同樣的核心功能,但仍然有一些區別,比如:LINQ to Object支持LAST 操作但是 LINQ to Entities不支持這個操作。
操作ObservableCollection
如果你仔細查看Local的返回類型,你會發現返回類型是ObservableCollection <TEntity>,這種類型集合被修改后就會通知其訂閱者,ObservableCollection在大部分的數據綁定(data-binding)場景下是有用的。同樣在你的應用程序需要知道什么時候新數據被加載到內存中也是有用的。
只要Local的內容發生變化,會引發CollectionChanged事件,比如:數據從數據庫讀取到內存中、新的對象被添加到DbContext,內存中的對象被標記為刪除等。
查看如下示例:Example 2-22
1 private static void ListenToLocalChanges()
3 using ( var context = new BreakAwayContext())
4 {
5 context.Destinations.Local.CollectionChanged += (sender, args) =>
6 {
7 if (args.NewItems != null)
8 {
9 foreach (Destination item in args.NewItems)
10 {
11 Console.WriteLine( " Added: " + item.Name);
12 }
13 }
14
15 if (args.OldItems != null)
16 {
17 foreach (Destination item in args.OldItems)
18 {
19 Console.WriteLine( " Removed: " + item.Name);
20 }
21 }
22 };
23
24 context.Destinations.Load();
25 }
26 }
以上代碼演示的是當有數據被添加或刪除的時候,會打印出被添加或刪除的數據,然后再從數據庫中重新加載一次Destinations的數據到內存中。
如果你的界面需要根據數據的變化及時的要刷新顯示,那這個事件將是非常便利的。
一些UI框架,比如WPF,會自動幫你做好這方面的工作:如果你綁定Local數據到WPF控件ListBox,無論何時何處數據變化了,ListBox都會及時跟新顯示。
注意:如果你對Local數據運行LINQ查詢的話,查詢返回的結果將不再是ObservableCollection類型的,那你懂的,把這個結果綁定給WPF ListBox,ListBox就不會自動跟新 顯示了,將需要你手動注冊OnCollectionChanged事件來處理刷新邏輯!
加載相關數據(Loading Related Data)
到現在為止我們處理的都是單一類型的實體數據,比如Destinations,但如果我們編寫一個真實的應用的話,我們肯定想知道Destinations的Lodgings,如果我們想獲取Destinations相關聯的Lodgings,這就意味着我們要處理關聯數據。
我們將需要把相關的數據載入到內存中以讓我們能夠操作,這里有三個方法你可用來加載相關數據:Lazy Loading、Eager Loading、Explicit Loading。這三種方法可能會返回同樣的結果,但對性能的影響是有一些區別的,決定使用哪個取決於不同的使用場景,並不是一錘子買賣,下面我們詳細探討這三個方法。
Lazy Loading
就是我們常說的延遲加載:你試圖訪問關聯數據的時候,EF會自動為你獲取這部分數據。比如:如果你有一個叫Grand Canyon的Destination被加載了,之后你想使用Destination的Lodgings屬性,EF將會自動的對數據庫發出一個查詢請求來加載特定Destination的Lodgings,表面看起來好像Lodgings屬性好像很自然的就可以隨意獲取訪問。
EF內部是通過dynamic proxy來實現Lazy Loading的:當EF返回一個查詢結果時,它會創建用戶定義的類的實例並用數據庫返回的數據填充這些實例。EF有一種能力能夠在運行時動態的創建一個新的類型,這個新的類型繼承自用戶定義的POCO類(關於POCO類各位自行補腦)。這個新類的行為就像是用戶POCO類的動態代理(dynamic proxy)一樣。它會重寫用戶POCO類的導航屬性並包含一些額外的邏輯在里面,這些邏輯是指:當這些屬性被訪問時從數據庫獲取數據。對用戶來說這些是內部機制,用戶也不需要關心是否在運行時會有一個dynamic proxy存在。EF take care it!
注意:DbContext有一個配置選項可用來決定是否開啟Lazy Loading:DbContext.Configuration.LazyLoadingEnabled。默認屬性是True。
為了能夠使用dynamic proxies也即支持Lazy Loading,你的類必須符合一些要求。如果這些要求不符合,EF將不會為你的類創建dynamic proxy,而只是簡單返回你的類的實例,而這是不能擁有Lazy Loading能力的。
這些要求包括:
- 你的POCO類必須是public並能夠被繼承的(言下之意么不能是Sealed的)
- 你想要擁有Lazy Loading的導航屬性字段必須是virtual的,所以EF才能夠重寫它們。
Example 2-23
1 private static void TestLazyLoading()
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 Console.WriteLine( " Grand Canyon Lodging: ");
12 if (canyon.Lodgings != null)
13 {
14 foreach ( var lodging in canyon.Lodgings)
15 {
16 Console.WriteLine(lodging.Name);
17 }
18 }
19 }
20 }
運行以上示例,這段代碼是不會打印出Lodging的名稱的,原因我想你們也知道了,因為我們最初的Destination類的Lodging導航屬性不是virtual的,這就導致EF不能重寫這個導航屬性,就不能支持Lazy Loading功能。修改很簡單:
再次運行程序將會不負所望。
以上代碼運行時,EF發送了給數據庫兩個查詢:第一個,當代碼調用Single方法時會向數據庫查詢名稱為Grand Canyon的Destination,記住Single方法使用的是SELECT TOP(2)查詢來保證只有一個結果。第二個,查詢Grand Canyon Destination的所有相關的Lodgings。
Multiple Active Result Sets
在EF對數據庫運行查詢時,當你第一次從查詢讀取數據時,它並沒有帶回所有的數據。每行數據都是按需從數據庫加載的。這就意味着你遍歷查詢結果時,這個查詢仍然是活動的並且當你遍歷時數據會被從數據庫取出來。
Lazy Loading功能在你遍歷查詢結果時是很常用的。比如,當你查詢所有的Destination后,你用foreach遍歷查詢結果,在這個遍歷過程中,你再去訪問Lodgings屬性,這個屬性就會被從數據庫延遲加載。這就表示對Lodgings的查詢執行時對所有Destingation的查詢仍然是活動的。
Multiple Active Result Sets AKA MARS 是SQL Server功能,它允許對同一個數據庫連接有多個活動查詢。Code First模式下創建一個數據庫連接時是默認開啟MARS。
如果你未開啟MARS同時你的代碼試着運行兩個活動查詢,你將會收到一個異常。這個異常取決於觸發第二個查詢的操作,但是inner exception將會是“There is already an open DataReader associated with this Command whick must be closed first”。
Lazy Loading的一些缺點
到現在為止,你們會覺得Lazy Loading會非常簡單,幾乎不用做什么事情,只要你想要什么數據,EF都會幫你加載好,但這也有不好的地方,不正確的使用Lazy Loading會導致很多對數據庫的查詢執行。比如,你可能加載了50個Destination之后訪問每個Destination的Lodgings。這將導致對數據庫的51次查詢:一次是獲得50個Destination,其余50次是查詢50個Destination相關的Lodgings。為了防止這種情況發生,就要講到接下來的另一種加載方式:Eager Loading。
ps:你也可以選擇關閉自動開啟的Lazy Loading,方法前面已經講過,關閉之后,就算你的導航屬性設置成virtual,Lazy Loading也不起效果。
--這一篇就到這了,下一篇是DbContext查詢的最后一篇