DbContext 查询(三)
接上一篇《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查询相关的功能基本探讨完了,后续博客我们继续探讨下对实体的增删改的基本操作。