Entity Framework 查詢使用集成查詢,簡稱LINQ。LINQ是一個查詢框架,並不限於Entity Framework使用,同樣不限於數據庫。LINQ Provider 負責將LINQ查詢翻譯成對數據的查詢,然后返回查詢結果。Entity Framework的LINQ Provider是LINQ to Entities,它將LINQ查詢翻譯成目標數據庫的SQL查詢語句。
除了LINQ,Entity Framework還支持基於文本的查詢Entity SQL,簡稱ESQL。ESQL通常使用在需要動態構造查詢的情況下。由於ESQL不常用,沒有直接暴露在DbContext API。如果需要使用ESQL,需要使用IObjectContextAdapter接口訪問ObjectContext API。
一、用到的Model
本篇文章使用BAGA模型,BAGA模型的完整代碼可以點擊這里下載,下面給出兩個主要實體的代碼。
Destination實體:
1: [Table("Locations", Schema = "baga")]
2: public class Destination
3: {
4: public Destination()
5: {
6: this.Lodgings = new List<Lodging>();
7: }
8:
9: [Column("LocationID")]
10: public int DestinationId { get; set; }
11: [Required, Column("LocationName")]
12: [MaxLength(200)]
13: public string Name { get; set; }
14: public string Country { get; set; }
15: [MaxLength(500)]
16: public string Description { get; set; }
17: [Column(TypeName = "image")]
18: public byte[] Photo { get; set; }
19: public string TravelWarnings { get; set; }
20: public string ClimateInfo { get; set; }
21:
22: public virtual List<Lodging> Lodgings { get; set; }
23: }
Lodging實體:
1: public class Lodging
2: {
3: public int LodgingId { get; set; }
4: [Required]
5: [MaxLength(200)]
6: [MinLength(10)]
7: public string Name { get; set; }
8: public string Owner { get; set; }
9: public decimal MilesFromNearestAirport { get; set; }
10:
11: [Column("destination_id")]
12: public int DestinationId { get; set; }
13: public Destination Destination { get; set; }
14: public List<InternetSpecial> InternetSpecials { get; set; }
15: public Nullable<int> PrimaryContactId { get; set; }
16: [InverseProperty("PrimaryContactFor")]
17: [ForeignKey("PrimaryContactId")]
18: public Person PrimaryContact { get; set; }
19: public Nullable<int> SecondaryContactId { get; set; }
20: [InverseProperty("SecondaryContactFor")]
21: [ForeignKey("SecondaryContactId")]
22: public Person SecondaryContact { get; set; }
23: }
二、查詢所有數據
查詢所有的數據有以下兩種方式:
第一種:
1: private static void PrintAllDestinations()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: foreach (var destination in ctx.Destinations)
6: {
7: Console.WriteLine(destination.Name);
8: }
9: }
10: }
第二種:
1: private static void PrintAllDestinations()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: var destinations = ctx.Destinations.ToList();
6: foreach (var destination in destinations)
7: {
8: Console.WriteLine(destination.Name);
9: }
10: }
11: }
首先,上面兩種方式生成的SQL是相同的,下面說一下它們之間的區別:
第一種情況,當應用程序請求第一條結果時,查詢語句發送到數據庫,也就是在foreach第一次循環期間,但是EF沒有一次返回所有的數據。這是查詢保持激活狀態,當需要數據時就從數據庫中讀取。這種情況下需要注意的是,每次對DbSet的內容進行遍歷都會查詢數據庫。如果不停地從數據庫中查詢相同的數據,就會出現明顯的性能問題,避免上述問題的方法是使用第二種方式。
第二種情況通過使用ToList()一次從數據庫中查詢得到所有的數據存放到內存中,不管反復查詢多少次,都是在內存中進行的操作,因此不存在上述提到的性能問題。
另外,發送到數據庫中的查詢是查找DbSet中的項目,遍歷DbSet只能得到存在於數據庫中的項目,任何停留在內存還沒保存到數據庫中的項目都不會出現在查詢的結果中。
上面最后一句話用程序表達就是下面的程序不會打印出名字為“杭州”的目的地,因為它還沒有保存到數據庫中。
1: using (var ctx = new BreakAwayContext())
2: {
3: ctx.Destinations.Add(new Destination()
4: {
5: Name = "杭州",
6: Country = "中國"
7: });
8: var destinations = ctx.Destinations.ToList();
9: foreach (var destination in destinations)
10: {
11: Console.WriteLine(destination.Name);
12: }
13: }
三、查找單個對象
查詢單個對象最常見的情況就是根據主鍵來查詢。為此,DbContext API提供了Find方法。如果Find根據提供的主鍵值找到相應的對象就將它返回,如果不存在就返回Null。
Find不僅查詢數據庫,而且還查詢新添加的沒有保存到數據庫中的對象。Find使用以下規則查找對象:
1.在內存中查找從數據庫中加載或附加到上下文的已存在的實體。
2.查找新添加的還沒有保存到數據庫中的對象。
3.在數據庫中查找還沒有加載到內存中的實體。
看下面的程序:
1: static void FindDestination()
2: {
3: Console.WriteLine("請輸入目的地的Id:");
4: var id = int.Parse(Console.ReadLine());
5: using (var ctx = new BreakAwayContext())
6: {
7: ctx.Destinations.Add(new Destination()
8: {
9: DestinationId = 3,
10: Name = "杭州",
11: Country = "中國"
12: });
13: var destination = ctx.Destinations.Find(id);
14: if (destination == null)
15: {
16: Console.WriteLine("目的地不存在!");
17: }
18: else
19: {
20: Console.WriteLine(destination.Name);
21: }
22: }
23: }
在上面的程序中,DestinationId的值是由數據庫自動生成的,為了演示Find可以查找沒有保存到數據庫的對象,我手動分配了一個值:3 給它,數據庫中也存在DestinationId為3的記錄。運行上面的程序,輸入3,結果如下圖所示:
下面看一下Single方法。
Single方法適用於不根據主鍵進行查詢或查詢時加載相關實體的情況。
1: static void FindSpain()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: var spain = ctx.Destinations.Single(t => t.Name == "西班牙");
6: Console.WriteLine(spain.Description);
7: }
8: }
如果Single查詢沒有返回結果或返回的結果多於一個就會拋出異常。
SingleOrDefault方法
SingleOrDefault和Single的區別是如果查詢沒有結果返回,那么SingleOrDefault返回null,Single則拋出異常。但是如果返回的結果多於一個,那么兩者都會拋出異常。
First和FirstOrDefault方法
如果不關心是否有多個結果,僅僅取得第一條,就是用First或FirstOrDefault方法。
如果查詢沒有結果返回,First會拋出異常,FirstOrDefault返回null。
四、查詢本地數據(內存中的數據)
前邊提到了Find方法,它在從數據庫中查詢數據之前會先查詢內存中的數據,不足之處是它只能根據主鍵來查詢,但是我們會經常性的對已經存在於內存中的數據進行復雜查詢並且能被DbContext追蹤。
這樣做的原因主要可能是,當需要的數據已經加載到內存中了,要避免發送多個查詢到數據庫。前面使用ToList()方法將查詢的結果復制到了List中,這種方法在同一個代碼塊中使用起來沒有問題,但是在應用程序的多個地方使用的話就要到處傳遞這個List了,顯然這樣使用起來有點亂。舉個例子:當應用程序加載時,從數據庫中加載所有的Destinations,應用程序不同的地方都會對它進行不同的查詢,有的地方可能顯示所有的Destinations,有的地方可能根據Name排序,有的地方可能根據Country條件過濾查詢。
另一個原因就是希望查詢的結果中包含新添加的數據(未保存到數據庫中的數據)。ToList()方法總是發送查詢到數據庫,也就是說沒有保存到數據庫中的數據不能包含在查詢結果中。
上面提到的兩種情況,使用本地查詢都能解決。查詢內存中的數據使用Local屬性,Local屬性返回所有從數據庫中加載的數據以及新添加的數據(未保存到數據庫)。任何被標記為Deleted,但是還沒有從數據庫中刪除的數據都會被過濾掉。
下面看個例子,先了解一下本地查詢:
1: static void GetLocalDestinationCount()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: int count = ctx.Destinations.Local.Count;
6: Console.WriteLine("內存中Destination的個數:" + count);
7: foreach (var destination in ctx.Destinations)
8: {
9: }
10: count = ctx.Destinations.Local.Count;
11: Console.WriteLine("內存中Destination的個數:" + count);
12: }
13: }
運行結果如下圖所示:
使用Load方法加載數據到內存
前邊的例子使用foreach循環將數據加載到內存中,僅僅是加載數據,這樣多少有點不稱職,而且有那么一點點不清楚代碼是干什么用的。幸好DbContext提供了Load方法,上面的例子使用Load方法如下:
1: static void GetLocalDestinationCount()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: int count = ctx.Destinations.Local.Count;
6: Console.WriteLine("內存中Destination的個數:" + count);
7: ctx.Destinations.Load();
8: count = ctx.Destinations.Local.Count;
9: Console.WriteLine("內存中Destination的個數:" + count);
10: }
11: }
Load方法是在IQueryable<T>上的擴展方法,因此可以加載指定條件的數據到內存中。方法如下所示:
1: ctx.Destinations.Where(t=>t.Country=="中國").Load();
上面的代碼是加載Destination在中國的數據到內存中。
使用ObservableCollection
Local屬性返回的類型是ObservableCollection<TEntity>,這種類型的集合,無論往集合中添加還是移除對象,都允許訂閱通知。熟悉WPF的,想必對ObservableCollection<TEntity>已經非常了解了。
當Local的內容變化時,Local會激發CollectionChanged事件,包括以下情況:通過查詢從數據庫中加載數據,將一個新的對象添加到DbContext,或已經存在內存的對象被標記為刪除。
下面看個例子:
1: static void ListenToLocalChanges()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: ctx.Destinations.Local
6: .CollectionChanged += (sender, args) =>
7: {
8: //添加到Local中的項
9: if (args.NewItems != null)
10: {
11: foreach (Destination item in args.NewItems)
12: {
13: Console.WriteLine("Added: " + item.Name);
14: }
15: }
16: //從Local中移除的項
17: if (args.OldItems != null)
18: {
19: foreach (Destination item in args.OldItems)
20: {
21: Console.WriteLine("Removed: " + item.Name);
22: }
23: }
24: };
25: //加載國家在中國的Destination到Local中
26: ctx.Destinations.Where(t => t.Country == "中國").Load();
27: //查詢Id為3的Destination,也添加到了Local中
28: var destination = ctx.Destinations.Find(3);
29: if (destination != null)
30: {
31: //從Local中移除Id為3的Destination
32: ctx.Destinations.Remove(destination);
33: }
34: int count = ctx.Destinations.Local.Count;
35: Console.WriteLine("內存中Destination的個數:" + count);
36: }
37: }
上面的代碼注冊了CollectionChanged事件,新添加到Local中或從Local中的項都打印出來。
運行結果如下圖所示:
五、加載關聯數據
加載關聯的數據有3種方法:延遲加載、預先加載、顯示加載。先來看延遲加載。
延遲加載
延遲加載在程序中是最顯而易見的,例如,加載了一個Destination,如果想使用Destination的Lodgings屬性,EF會自動發送查詢到數據庫來加載位於這個Destination的所有Lodgings。
EF的延遲加載使用的是動態代理,下面是它的工作過程:
當EF返回查詢的結果時,它會創建類的實例並使用從數據庫返回的數據進行填充。EF具有在運行時動態創建派生自POCO類的新類的能力。新類充當POCO類的代理,稱為動態代理。當屬性被訪問時,它會重寫POCO類的導航屬性並包含一些從數據庫獲取數據的邏輯。
使用動態代理完成延遲加載,需要滿足一定的規則。如果不能滿足規則,EF就不能創建類的動態代理,只能返回不能進行延遲加載的POCO類的實例。下面是要滿足的規則:
1.POCO類必須是public的且不能為sealed。
2.需要延遲加載的導航屬性必須標記為virtual,以便EF可以重寫導航屬性加入延遲加載的邏輯。
延遲加載的缺點
不合理的使用延遲加載會導致大量的查詢發送到數據庫。例如,加載50個Destination,然后訪問每個Destination的Lodgings屬性,這樣就會有51條查詢語句發送到數據庫。合理的做法是使用一條查詢語句加載所有的數據,這也正是預先加載要做的。
關閉延遲加載
關閉延遲加載可以去掉導航屬性的virtual標記,還可以將DbContext.Configuration.LazyLoadingEnabled屬性設置false,設置false之后即使導航屬性標記為virtual,延遲加載也不會起作用。
預先加載
預先加載關聯的數據需要你告訴EF關聯什么數據,然后EF在產生的SQL語句中使用Join,一條語句加載所有的數據。告訴EF關聯的數據使用Include方法。看下面的例子:
1: static void TestEagerLoading()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: //var allDestinations = ctx.Destinations.Include("Lodgings");
6: //使用Include的泛型方法,需要引進System.Data.Entity命名空間
7: var allDestinations = ctx.Destinations.Include(t => t.Lodgings);
8: foreach (var destination in allDestinations)
9: {
10: Console.WriteLine(destination.Name);
11: foreach (var lodging in destination.Lodgings)
12: {
13: Console.WriteLine(" - " + lodging.Name);
14: }
15: }
16: }
17: }
運行結果如下圖:
可以在單個查詢中包含多個關聯的數據集合,比如說可以在查詢Lodgings時,關聯PrimaryContact和Photo,代碼如下:
1: ctx.Lodgings.Include(t => t.PrimaryContact.Photo);
PrimaryContact是Person類,Photo是PersonPhoto類,具體可以下載本文的源碼查看。
還有一種情況就是,查詢Destinations,要關聯Lodgings,同時關聯每個Lodging的PrimaryContact,代碼如下:
1: ctx.Destinations.Include(t => t.Lodgings.Select(l => l.PrimaryContact));
在一個查詢中,還可以包含多次Include,例如,查詢Lodgings時,既想包含PrimaryContact,還想包含SecondaryContact,代碼如下所示:
1: ctx.Lodgings.Include(t => t.PrimaryContact).Include(t => t.SecondaryContact);
預先加載的缺點
使用預先加載,有一件事情一定要牢記就是越少的查詢並不總是好的。減少查詢的數量是以查詢的簡單性為代價的。包含的關聯數據越多,發送到數據庫查詢中關聯的數量也就會越多,結果就是查詢變得更慢和更復雜。如果只是需要關聯少量的數據,多個簡單的查詢常常要比復雜的查詢要快。
顯示加載
顯示加載在加載關聯數據上和延遲加載相似,都是在主數據加載完后,再加載關聯的數據。與延遲加載不同的是,它不會自動發生,需要手動調用方法加載數據。
下面可能是你會選擇顯示加載而不選擇延遲加載的原因:
1.不需要標記導航屬性為virtual。這一改變對一些人來說沒有意義,而對另外一些人來說數據訪問技術需要改變POCO類非常不理想,使用顯示加載則不需要改變POCO類。
2.使用已有的類庫,導航屬性沒有標記為virtual。
3.顯示加載允許知道查詢什么時候發送到數據庫。延遲加載會潛在的產生很多查詢,而顯示加載何時何地運行查詢都是非常明顯的。
顯示加載使用DbContext.Entry方法。一旦擁有了給定實體的entry,就可以使用Collection和Reference方法在導航屬性上獲取信息和執行操作。一個可行的操作就是Load方法,它發送一個查詢到數據加載導航屬性的內容。
下面看一下代碼,加載集合導航屬性:
1: static void TestExplicitLoading()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: var hongkong = ctx.Destinations.Single(t => t.Name == "香港");
6: ctx.Entry(china).Collection(d => d.Lodgings).Load();
7: Console.WriteLine("香港住宿:");
8: foreach (var lodging in hongkong.Lodgings)
9: {
10: Console.WriteLine(lodging.Name);
11: }
12: }
13: }
加載引用導航屬性代碼:
1: var lodging = ctx.Lodgings.First();
2: ctx.Entry(lodging).Reference(l => l.PrimaryContact).Load();
檢查導航屬性是否加載:
1: static void TestIsLoaded()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: var hongkong = ctx.Destinations.Single(t => t.Name == "香港");
6: var entry = ctx.Entry(hongkong);
7: Console.WriteLine(
8: "加載前: {0}",
9: entry.Collection(d => d.Lodgings).IsLoaded);
10: entry.Collection(d => d.Lodgings).Load();
11: Console.WriteLine(
12: "加載后: {0}",
13: entry.Collection(d => d.Lodgings).IsLoaded);
14: }
15: }
查詢集合導航屬性的內容
到目前為止,已經看到了加載集合導航屬性的整個內容。如果想篩選導航屬性的內容,可以先加載到內存中進行操作,但是,如果只想加載導航屬性內容的一個子集或者只求導航屬性內容的個數抑或是一些其他計算,在數據庫中計算比加載所有的數據到內存中更有意義。
使上述問題變得更有意義的是Query方法。假設想查找所有位於法國且離最近機場距離小於10里的Lodgings。
下面看一下代碼:
1: static void QueryLodgingDistance()
2: {
3: using (var ctx = new BreakAwayContext())
4: {
5: var france = ctx.Destinations.First(t => t.Country == "法國");
6: var lessTenMilesLodgings = ctx.Entry(france)
7: .Collection(t => t.Lodgings)
8: .Query()
9: .Where(t => t.MilesFromNearestAirport < 10);
10: foreach (var lodging in lessTenMilesLodgings)
11: {
12: Console.WriteLine(lodging.Name);
13: }
14: }
15: }
由上圖可以看到生成的SQL,where條件里包含了MilesFromNearestAirport<10,如果寫成如下形式:
1: var lessTenMilesLodgings = france.Lodgings.Where(t => t.MilesFromNearestAirport < 10);
會將DestinationId為3的Lodging都加在到內存中,然后在內存中篩選小於10里的數據。
通過文檔可以發現Query()返回的是IQueryable<T>類型的。求集合導航屬性內容的個數,只需要在Query()方法使用Count()擴展方法即可,代碼如下:
1: var count = ctx.Entry(france)
2: .Collection(t => t.Lodgings)
3: .Query()
4: .Count();
顯示加載導航屬性內容的子集
Query方法和Load方法可以組合使用。比如,可能只想加載位於法國且名字中包含“Martinique”的Lodgings,代碼如下:
1: ctx.Entry(france)
2: .Collection(t => t.Lodgings)
3: .Query()
4: .Where(t => t.Name.Contains("Martinique"))
5: .Load();
注意,調用Load方法不會清除已經存在於導航屬性中的任何對象。如果已經加載了位於法國且Name中包含“Martinique”的Lodgings,然后又加載了包含“Miquelon”的Lodgings,那么Lodgings導航屬性將包含Martinique和Miquelon。
六、結束語
本文使用的完整源碼和數據庫到這里下載:http://www.ef-community.com/forum.php?mod=viewthread&tid=367&extra=page%3D1
點擊查看《Entity Framework實例詳解》系列的其他文章。
如果遇到問題,可以加群:276721846 進行討論。
外歡迎大家訪問Entity Framework社區,網址是www.ef-community.com。