首页 > 代码库 > DbContext 查询(二)
DbContext 查询(二)
接上一篇《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查询的最后一篇
DbContext 查询(二)