【查询】—Entity Framework实例详解
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。