当前位置: 首页 > 图文教程 > 网络编程 > ASP.NET > LINQ学习笔记:Join和Group Join

ASP.NET
Validation服务器控件:ValidationSummary控件
ASP.NET教程:URL重写的实现
ASP.NET巧妙实现无刷新更换CSS样式
ASP.NET教程:eval()函数详解
LINQ学习笔记:结构化且类型安全的查询
LINQ学习笔记:Lambda表达式
LINQ学习笔记:复合查询和Lambda表达式语法
LINQ学习笔记:查询是怎么执行的
LINQ学习笔记:子查询和延迟执行
LINQ学习笔记:创建更加复杂查询的策略
LINQ学习笔记:对象初始化器
LINQ学习笔记:解释查询(Interpreted Queries)
LINQ学习笔记:表达式树
LINQ学习笔记:过滤Filtering
LINQ学习笔记:选取Select
LINQ学习笔记:Join和Group Join
LINQ学习笔记:排序Ordering
LINQ学习笔记:分组Grouping
LINQ学习笔记:Set操作符
LINQ学习笔记:转换方法

ASP.NET 中的 LINQ学习笔记:Join和Group Join


出处:互联网   整理: 软晨网(RuanChen.com)   发布: 2009-09-28   浏览: 172 ::
收藏到网摘: n/a

连接Join

主要方法:

Join:  应用一个搜寻策略去匹配两个集合中的元素, 并返回一个扁平的结果集, SQL对应语法为INNER JOIN

Group Join: 同上, 但返回的是一个层级的结果集, SQL对应语法为INNER JOIN, LEFT OUTER JOIN

概要

Join和GroupJoin将两个输入序列编织成一个单一的输出序列, Join返回一个扁平的输出结果集, GroupJoin则返回一个层级结果集.

Join和GroupJoin提供了Select与SelectMany之外的另一个选择.  Join和GroupJoin的优势在于针对本地查询更加高效,因为它首先将内部序列加载到一个lookup目录当中,避免重复枚举每一个内部元素. 它的劣势在于只能对应于INNER JOIN和LEFT OUTER JOIN, 交叉连接和非等连接依然还是需要Select和SelectMany. 而对于LINQ to SQL, Join和GroupJoin对比Select和SelectMany并没有提供任何额外的好处.

Join

Join操作符执行一个内连接(inner join), 输出一个扁平序列

最简单的演示Join用处的做法是使用LINQ to SQL, 以下的查询列出所有的客户以及他们的订单信息而没有使用关联属性

 1: IQueryable<string> query =
 2:  
 3: from c in dataContext.Customers
 4:  
 5: join p in dataContext.Purchases
 6:  
 7: on c.ID equals p.CustomerID
 8:  
 9: select c.Name + ” bought a “ + p.Description;

 

结果与我们使用SelectMany查询得到的结果一致

要了解Join相对于SelectMany额外的好处, 我们必须将它转换为本地查询, 以下的例子先将所有的客户和采购订单转换为数组, 然后再做进一步的查询:

 1: Customer[] customers = dataContext.Customers.ToArray( );
 2:  
 3: Purchase[] purchases = dataContext.Purchases.ToArray( );
 4:  
 5: var slowQuery =
 6:  
 7: from c in customers
 8:  
 9: from p in purchases where c.ID == p.CustomerID
 10:  
 11: select c.Name + ” bought a “ + p.Description;
 12:  
 13: var fastQuery =
 14:  
 15: from c in customers
 16:  
 17: join p in purchases on c.ID equals p.CustomerID
 18:  
 19: select c.Name + ” bought a “ + p.Description;

 

虽然两种方式返回的结果集是一样的, 但是Join查询执行得更快一些, 因为它在Enumerable当中的实现预加载了内联集合(purchases)到一个有键的字典中

Join执行一个内连接操作, 这意味着那些没有采购订单的客户将被排除在输出结果之外. 使用inner join, 你可以将inner和outer序列互换, 并且仍然可以得到同样的结果:

 1: from p in purchases
 2:  
 3: join c in customers on p.CustomerID equals c.ID

 

我们可以增加更多的join语句到相同的查询中, 例如, 假设每个采购订单包含一或多个的采购明细, 我们可以像下面这样将他们连接在一起:

 1: from c in customers
 2:  
 3: join p in purchases on c.ID equals p.CustomerID
 4:  
 5: join pi in purchaseItems on p.ID equals pi.PurchaseID

 

Purchases在第一个连接中扮演了inner序列, 而在第二个连接中则扮演了outer序列的角色, 我们可以使用嵌套foreach得到相同的结果, 但是效率不高:

 1: foreach (Customer c in customers)
 2:  
 3: foreach (Purchase p in purchases)
 4:  
 5: if (c.ID == p.CustomerID)
 6:  
 7: foreach (PurchaseItem pi in purchaseItems)
 8:  
 9: if (p.ID == pi.PurchaseID)
 10:  
 11: Console.WriteLine (c.Name + “,” + p.Price +
 12:  
 13: “,” + pi.Detail);

 

多主键连接

我们可以使用匿名类型来进行多主键链接操作:

 1: from x in seqX
 2:  
 3: join y in seqY on new { K1 = x.Prop1, K2 = x.Prop2 }
 4:  
 5: equals new { K1 = y.Prop3, K2 = y.Prop4 }

 

为了能够运行这个查询, 两个匿名类型的结构必须是相同的. 编译器会将它们实现为相同的内部类型, 因此多主键链接能够运行.

Lambda方式的连接

以下的示例使用了复合查询语法:

 1: from c in customers
 2:  
 3: join p in purchases on c.ID equals p.CustomerID
 4:  
 5: select new { c.Name, p.Description, p.Price };

 

使用Lambda表达式的话则可以改成这样:

 1: customers.Join ( // outer collection
 2:  
 3: purchases, // inner collection
 4:  
 5: c => c.ID, // outer key selector
 6:  
 7: p => p.CustomerID, // inner key selector
 8:  
 9: (c, p) => new // result selector
 10:  
 11: { c.Name, p.Description, p.Price }
 12:  
 13: );

 

最后的结果选择器表达式创建了输出序列中的每一个元素, 如果你还有额外的查询语句需要去执行, 例如orderby:

 1: from c in customers
 2:  
 3: join p in purchases on c.ID equals p.CustomerID
 4:  
 5: orderby p.Price
 6:  
 7: select c.Name + ” bought a “ + p.Description;

 

在Lambda方式中, 我们就必须在结果选择器表达式中去生成一个临时的匿名类型. 这样可以保持c和p在同一个join作用范围内:

 1: customers.Join ( // outer collection
 2:  
 3: purchases, // inner collection
 4:  
 5: c => c.ID, // outer key selector
 6:  
 7: p => p.CustomerID, // inner key selector
 8:  
 9: (c, p) => new { c, p } ) // result selector
 10:  
 11: .OrderBy (x => x.p.Price)
 12:  
 13: .Select (x => x.c.Name + ” bought a “
 14:  
 15: + x.p.Description);

 

可以看得出来复合查询语法更加的直观一点, 这也是在使用joining操作时推荐的做法.

GroupJoin

GroupJoin功能与Join类似, 只不过GroupJoin返回的结果集是一个层级结构, 而不是一个扁平的结构. 对于复合语法来说, GroupJoin和Join是一样的, 只不过它通常会跟着一个into关键字:

 1: var query =
 2:  
 3: from c in customers
 4:  
 5: join p in purchases on c.ID equals p.CustomerID
 6:  
 7: into custPurchases
 8:  
 9: select custPurchases;

 

只有当into语句出现在一个join语句之后它才会被翻译成GroupJoin. 如果是跟在一个select或者group语句之后, 则意味着是查询延续. 这两个用法是非常不同的, 但它们有一个共同的特点: 两者都引入了新的查询变量

上述查询结果是一个序列的序列, 我们可以像下面这样来枚举它:

 1: foreach (IEnumerable purchaseSequence in query)
 2:  
 3: foreach (Purchase p in purchaseSequence)
 4:  
 5: Console.WriteLine (p.Description);

 

另外我们也可以在返回结果中来选取外部的查询变量:

 1: from c in customers
 2:  
 3: join p in purchases on c.ID equals p.CustomerID
 4:  
 5: into custPurchases
 6:  
 7: select new { CustName = c.Name, custPurchases }; //c可以被引用到

 

使用下面的select子查询也可以得到同样的结果:

 1: from c in customers
 2:  
 3: select new
 4:  
 5: {
 6:  
 7: CustName = c.Name,
 8:  
 9: custPurchases =
 10:  
 11: purchases.Where (p => c.ID == p.CustomerID)
 12:  
 13: };

 

默认情况下, GroupJoin相当于一个左外连接. 为了得到一个inner join, 我们必须在custPurcahse上面做一层过滤:

 1: from c in customers join p in purchases
 2:  
 3: on c.ID equals p.CustomerID
 4:  
 5: into custPurchases
 6:  
 7: where custPurchases.Any( )
 8:  
 9: select ...

 

在group-join之后的into操作作用于内部元素的子序列, 而不是每一个单独的子元素, 因此如果你要过滤每一个单独的采购单, 你必须在joining之前调用Where

 1: from c in customers
 2:  
 3: join p in purchases.Where (p2 => p2.Price > 1000)
 4:  
 5: on c.ID equals p.CustomerID
 6:  
 7: into custPurchases ...

 

扁平的Outer Joins

当你想得到一个Outer Join同时输出扁平的结果集的时候, 这会是一个两难的境地. GroupJoin会提供给你Outer Join; Join提供给你扁平的结果集. 因此, 解决方案是先调用GroupJoin, 然后在每一个子序列上面调用DefaultIfEmpty, 最后调用SelectMany:

 1: from c in customers
 2:  
 3: join p in purchases on c.ID equals p.CustomerID
 4:  
 5: into custPurchases
 6:  
 7: from cp in custPurchases.DefaultIfEmpty( )
 8:  
 9: select new
 10:  
 11: {
 12:  
 13: CustName = c.Name,
 14:  
 15: Price = cp == null ? (decimal?) null : cp.Price
 16:  
 17: };

 

当Purchases为空的时候DefaultIfEmpty会得到一个null值. 第二个from语句被翻译成SelectMany, 它扩展和压扁了所有的purchase, 并将它们联合在一起形成purchase元素所属的单一序列

连接和Lookups

Enumerable当中的Join和GroupJoin使用两个步骤来工作. 首先, 它们将inner序列加载到一个lookup当中, 然后再将一个outer序列和这个lookup组合在一起.

一个lookup是一个分组序列, 并且可以通过键直接访问, 或者你可以认为它是一个序列字典, 每一个键可以接受多个元素. 并且lookup是只读的, 定以如下:

 1: public interface ILookup :
 2:  
 3: IEnumerable>, IEnumerable
 4:  
 5: {
 6:  
 7: int Count { get; }
 8:  
 9: bool Contains (TKey key);
 10:  
 11: IEnumerable this [TKey key] { get; }
 12:  
 13: }

 

当处理本地集合的时候, 我们可以使用joining操作符作为一个额外的可选策略来手工创建或者查询lookups, 这允许你在多个查询上面重用相同的lookup.

ToLookup扩展方法创建了一个lookup. 以下的例子将所有的purchases加载到一个lookup当中去, 它们的关键是CustomerID字段.

 1: ILookup<int?,Purchase> purchLookup =
 2:  
 3: purchases. ToLookup (p => p.CustomerID, p => p);

 

第一个参数选择了键, 第二个参数选取那些作为值被加载到lookup的对象.

读取一个lookup就像在读取一个字典, 除非indexer返回了包含匹配项的序列, 而不是简单的匹配. 以下的例子枚举了ID是1的客户采购订单:

 1: foreach (Purchase p in purchLookup [1])
 2:  
 3: Console.WriteLine (p.Description);

 

在适当的位置使用lookup, 我们可以让SelectMany / Select查询执行得跟Join / GroupJoin一样有效率. 在一个lookup上面使用SelectMany与Join是等价的:

 1: from c in customers
 2:  
 3: from p in purchLookup [c.ID]
 4:  
 5: select new { c.Name, p.Description, p.Price };
 6:  
 7: Tom Bike 500
 8:  
 9: Tom Holiday 2000
 10:  
 11: Dick Bike 600
 12:  
 13: Dick Phone 300
 14: ...

 

增加一个DefaultIfEmpty的调用可以让此查询变成一个Outer Join:

 1: from c in customers
 2:  
 3: from p in purchLookup [c.ID].DefaultIfEmpty()
 4:  
 5: select new
 6:  
 7: {
 8:  
 9: c.Name,
 10:  
 11: Descript = p == null ? null :p.Description,
 12:  
 13: Price = p == null ? (decimal?) null :p.Price
 14:  
 15: };

 

在projection内读lookup与GroupJoin是等价的:

 1: from c in customers
 2:  
 3: select new {
 4:  
 5: CustName = c.Name,
 6:  
 7: CustPurchases = purchLookup [c.ID]
 8:  
 9: };

 

待续!