这是对“LINQ本质(再版)第二篇 运算”的补遗,主要讲述C#的LINQ表达式如何表示JOIN操作。(话说关于VB的LINQ表达式部分大家可以去催装配脑袋写。)
JOIN及相关操作使用LINQ表达式中较之调用扩展方法更为简洁,但语法的复杂度也是令人叹为观止,也使得我必须专门写一篇文章来补遗这一部分。
提示:在本篇文章中,所有的示例表达式均选用一个假想的数据源,其中一些单词出现的有:学生student,课程course,考试examination,成绩results,班级class。不会事先来声明这些数据的类型信息,对于简单的LINQ表达式,也不再描述其含义,因为相信看过这个系列前两篇文章并已经能够理解的同学,可以直接从LINQ表达式中推测出其含义以及这些东西的类型。让我们把注意力专注于LINQ表达式语法上。
提示:在本篇文章中,不会再时刻强调LINQ表达式的完整形式。这是因为这篇文章所讨论的LINQ表达式的形式将非常复杂,为了版面清晰考虑。同时,对上一篇文章已经理解透彻的同学而言,可以毫不费力的推导出完整的形式。
提示:在本篇文章中,由于主要讨论from和join两个子句,所以对于所有的LINQ表达式,将始终采用绿蓝色来突出下列关键字:from、join、on、equals。但对于in关键字,因为其语法形式大家应该已经非常熟悉,并且并不是本篇文章讨论的范畴,采用不醒目的加粗字体。
我们知道在SQL中一共有五种JOIN操作:INNER JOIN、LEFT OUTER JOIN、RIGHT OUTER JOIN、FULL OUTER JOIN、CROSS JOIN。
CROSS JOIN就是两个关系数据的笛卡尔积,首先说说CROSS JOIN在LINQ表达式中的表示。
LINQ表达式用多个from子句来表示CROSS JOIN,这与SQL有些类似(SQL用FROM table1, table2, … ,tablen来表示),语法如下:
from element1 in set1 from element2 in set2 …
同样的,from子句不能造成输出,所以只有from不是完整的LINQ表达式。一个完整的例子如下:
var r = from student in allStudents from course in allCourses select new { Student = student, Course = course };
当然,可以再进行筛选。
var r = from student in allStudents from course in allCourses where Examination.GetScore( student, course ) > 60 select new { Student = student, Course = course };
另外,多个from子句也可以获取集合的集合的元素,这里先给出一个例子,后面会详细说到:
var students = from @class in allClasses from student in @class.Students select student;
如果不想用LINQ表达式,则可以使用Enumerable.SelectMany扩展方法来实现同样的效果。如第一个LINQ表达式的对应的扩展方法调用可以采用:
var r = allStudents.SelectMany( student => allCourses.Select( new { Student = student, Course = course } ) );
其实直接对CROSS JOIN结果进行筛选就能得到INNER JOIN的结果,但这样的效率显然更差,LINQ表达式也提供了INNER JOIN的表达式。不过,其实很复杂,语法如下:
from element1 in set1 join element2 in set2 on key1 equals key2
很明显的,join子句的前半段与from是一样的,我们关注后半段,或者说on子句。其中的key1必须是element1通过计算的结果,key2必须是element2通过计算的结果;并且,key1的表达式中不能包含element2,key2的表达式中不能包含element1。比如说,下面的表达式片段是不合法的:
from student in allStrudents join results in courseResults on results.Student equals student
因为results和student的左右位置反了。但其他的元素,如两个集合,则左右表达式都可以出现。
值得注意的是,join子句必须也只能使用equals关键字来描述两个键的关系,换言之除了同等关系外,其他的关系都是不被支持的,其他关系可以用CROSS JOIN再筛选的方式实现。
注意:与现代的关系型数据库不同的是,直接执行LINQ表达式的时候一般不会自动进行优化,换言之在特定情况下,两个键的先后顺序,使用join子句和不使用join子句而用where筛选,都会影响到执行效率。
join子句也支持复合键的比较,只需要生成两个同构(同构要求属性的名称、类型和数量完全相同)的匿名类对象,系统会自动比较构成匿名类对象的所有属性是否一致:
from element1 in set1 join element2 in set2 on new { element1.Key1, element1.Key2 … } equals new { element2.Key1, element2.Key2 … }
如果有复杂的比较需求,建议自定义一个类型来作为比较键,用一个方法来获取:
from element1 in set1 join element2 in set2 on element1.GetKey() equals element2.GetKey()
小贴士:GetKey()可以是一个扩展方法。
但诸如下面这样的需求,是无法实现的:
from element1 in set1 join element2 in set2 on element1.Equals( element2 )
from element1 in set1 join element2 in set2 on Equality.Equals( element1, element2 )
复杂规则:
事实上join和from关键字并不一定要遵循如下的格式:
from element1 in set1 join element2 in set2 on key1 equals key2
C#的LINQ表达式支持的其实是:
set-expression join element2 in set2 on key1 equals key2
set-expression from element in set
那么什么是set-expression,简单的说,把一个完整的LINQ表达式砍掉造成输出的select和group by子句就是set-expression,比如说一个from子句是一个set-expression,再加上where也是set-expression,再加上orderby,还是set-expression。当然,显然的,多重from或者再加上join也都是set-expression。
譬如说,我们可以将上面的例子改一下,只取出男学生的成绩:
from student in allStudents where student.Gender == Gender.Male join results in courseResults on student equals results.Student
注意:如同刚才所说的,直接执行LINQ表达式的时候不会有谁自动帮你优化,所以在特定情况下,应该能明显觉察出将where放在前面会获得性能提升。关于LINQ表达式的优化问题,计划在系列的后续文章中详细讨论。
对于拥有select和group by子句的LINQ表达式来说,只需要加上into便能成为set-expression,关于into的语法在上一篇文章中已经谈过,不再赘述。
如果您看到这里还能很容易的看懂的话,那恭喜您。但千万不要高兴的太早,因为我们接下来即将迎接LINQ表达式中或许是最复杂的语法,分组连接。
在join子句的最后面加上into关键字即可表示分组连接。
into关键字我们在上篇文章中说过,用于接续表达式,避免写成嵌套模式。就在刚刚,我们还进一步剖析了其本质,用于将select和group by的结果转换为set-expression。但,现在你必须把之前的这一切都忘记,因为在join子句后面加上into的含义是完全不同的!
into关键字将改变join子句的行为。在没有into的时候,join子句将被映射成对Join方法的调用,加上into后,则变成了对GroupJoin方法的调用!
我们先来看一下join … into的语法:
from element1 in set1 join element2 in set2 on key1 equals key2 into element2Group
into子句的职责是将element2转换为element2Group。所以当你使用into关键字后,你会发现element2不见了。即下面的LINQ表达式是不合法的:
from student in allStudents join results in courseResults on student equals results.Student into resultsGroup select new { Student = student, Results = results };
因为results不复存在,取而代之的是resultsGroup。
这是第一个改变,第二个改变是结果项的改变。
当没有into关键字的时候,join的行为与我们熟知的INNER JOIN的行为并无二致。但加上into关键字后,行为有点儿类似于group by。其具体行为是,根据set1的每一项,找出匹配(满足连接条件)的set2的项集。也就是说对于每一个element1,找出key1 equals key2的所有element2,这些匹配的element2的集合就是element2Group。
其中有两点要注意:
1、对于某个在set1中的项,在set2中没有任何匹配,则element2Group会是一个空集合。
2、在set2中存在,但对于set1中的任何一项都不满足连接条件的set2的项,不会出现在结果中。
利用分组连接,我们就能够做出LEFT OUTER JOIN的结果集。不过语法,呃,,,,相当复杂。
在讨论LEFT OUTER JOIN的LINQ表达式之前,我们先回过头来看多个from的表达式。
其实不论是进行CROSS JOIN的from叠加:
from element1 in set1 from element2 in set2
还是查询集合的集合:
from element1 in set1 from element2 in element1
其本质都是一样的,可以看成是两个foreach的嵌套,比如说这个例子:
var students = from @class in allClasses from student in @class.Students select student;
我们可以看成:
from( var @class in allClasses )
{
from( var student in @class.Students )
{
select student;
}
}
如果把from换成foreach,而把select换成yield return会是怎样呢?
理解了这一点,我们继续下面的话题,怎样把GroupJoin变成LEFT OUTER JOIN。
首先我们要解决的问题就是把分组好的数据再拆开,我们刚刚说了,join后面加上into关键字后,即会对element2进行分组按照连接键分组,转换成element2Group,而传统的LEFT OUTER JOIN要求我们还是要把这个东西拆成一个个的项来与左边的对应项重新组合,所以我们在后面再加上一个from来解决:
from student in allStrudents join results in courseResults on student equals results.Student into resultsGroup
from results in resultsGroup
注意这样的语法正是我们刚刚提到的查询集合的集合的语法。
然后加上select以造成输出:
from student in allStudents join results in courseResults on student equals results.Student into resultsGroup
from _results in resultsGroup select new { Student = student, Results = _results }
小贴士:其实在这里_results变量可以直接命名为results,并不会与前面的results相冲突,因为它们虽然在同一个表达式中,却是在不同的作用域。
但这样得到的结果仍然是一个INNER JOIN的结果,因为对于retulsGroup的空集,第二个from子句后面的select是不会被执行的。
为了解决这个问题,微软提供了一个扩展方法:DefaultIfEmpty,事实上我甚至认为这个扩展方法除了用在这里并没有别的用处。
简单的说,这个扩展方法就是检查一个集合是不是空集,如果是一个空集,那么就返回一个包含集合类型默认值(default( T ) )的集合。否则就按原样输出集合。所以,正确的LEFT OUTER JOIN的LINQ表达式完整版为:
from student in allStudents join results in courseResults on student equals results.Student into resultsGroup
from results in resultsGroup.DefaultIfEmpty() select new { Student = student, Results = results }
不过,在这里我想说的是,我们不要被传统的SQL所束缚思想,事实上很多时候,LEFT OUTER JOIN是没有必要的,比如说我们要查询每个班级有多少学生:
var r = from @class in allClasses join student in allStudents on @class equals student.Class into studentGroup select new { Class = @class, studentGroup.Count() }
实现了LEFT OUTER JOIN后,RIGHT OUTER JOIN也能实现了,将join两边的集合倒过来即可,而这也是现在来看唯一的办法。
最后是FULL OUTER JOIN,这种JOIN并没有现成的解决方案。这也给我们留下了一个很好的问题,怎样利用LINQ提供的这些新特性,通过自己写一些简单的方法来漂亮的实现FULL OUTER JOIN的LINQ表达式呢?
预告:
LINQ本质系列下一篇文章暂定为“LINQ本质(再版)第三篇 执行”,深入延迟执行的本质,剖析Enumerable的各个扩展方法的执行过程,试图还原源代码(这些方法大都是yield语法写的,Reflector只能看到一大堆乱七八糟的匿名类型)。并试图分析不同的写法对性能的影响。