我有一份人名和他们的名字列表,以及一个人的身份证和姓氏列表 . 有些人没有名字,有些人没有姓氏;我想在两个列表上进行完全外连接 .
所以以下列表:
ID FirstName
-- ---------
1 John
2 Sue
ID LastName
-- --------
1 Doe
3 Smith
应该产生:
ID FirstName LastName
-- --------- --------
1 John Doe
2 Sue
3 Smith
我是LINQ的新手(如果我是跛脚的话,请原谅我)并找到了很多“LINQ Outer Joins”的解决方案,这些解决方案看起来非常相似,但实际上似乎是留下了外部联接 .
到目前为止,我的尝试是这样的:
private void OuterJoinTest()
{
List<FirstName> firstNames = new List<FirstName>();
firstNames.Add(new FirstName { ID = 1, Name = "John" });
firstNames.Add(new FirstName { ID = 2, Name = "Sue" });
List<LastName> lastNames = new List<LastName>();
lastNames.Add(new LastName { ID = 1, Name = "Doe" });
lastNames.Add(new LastName { ID = 3, Name = "Smith" });
var outerJoin = from first in firstNames
join last in lastNames
on first.ID equals last.ID
into temp
from last in temp.DefaultIfEmpty()
select new
{
id = first != null ? first.ID : last.ID,
firstname = first != null ? first.Name : string.Empty,
surname = last != null ? last.Name : string.Empty
};
}
}
public class FirstName
{
public int ID;
public string Name;
}
public class LastName
{
public int ID;
public string Name;
}
但这回归:
ID FirstName LastName
-- --------- --------
1 John Doe
2 Sue
我究竟做错了什么?
13 回答
这是一个扩展方法:
两个或多个表的完全外部联接:首先提取要加入的列 .
然后在提取的列和主表之间使用左外连接 .
我喜欢sehe的答案,但它不使用延迟执行(输入序列是由ToLookup调用急切枚举的) . 因此,在查看LINQ-to-objects的.NET源代码后,我想出了这个:
此实现具有以下重要属性:
延迟执行,在枚举输出序列之前不会枚举输入序列 .
仅枚举输入序列 .
保留输入序列的顺序,在某种意义上它将按照左序列的顺序产生元组,然后是右(对于左序列中不存在的键) .
这些属性非常重要,因为它们是FullOuterJoin的新手,但对LINQ经验丰富 .
正如您所发现的,Linq没有“外连接”结构 . 您可以获得的最接近的是使用您所述查询的左外连接 . 为此,您可以添加在联接中未表示的姓氏列表的任何元素:
在两个输入上执行内存中的流枚举,并为每一行调用选择器 . 如果当前迭代中没有相关性, one of the selector arguments will be null .
例:
需要IComparer作为相关类型,如果未提供,则使用Comparer.Default .
要求'OrderBy'应用于输入枚举
Update 1: providing a truly generalized extension method FullOuterJoin
Update 2: optionally accepting a custom IEqualityComparer for the key type
Update 3 :这个实现有recently become part of MoreLinq - 谢谢大家!
Edit 已添加
FullOuterGroupJoin
(ideone) . 我重用了GetOuter<>
实现,使得它的性能比它可能的要低一些,但是我现在已经高度优化了代码,而不是前沿优化的代码 .在 http://ideone.com/O36nWc 上看到它
打印输出:
您还可以提供默认值: http://ideone.com/kG4kqO
印刷:
使用的术语解释:
加入是从关系数据库设计中借用的术语:
A join 将重复
a
中的元素,因为b
中的元素与相应的键重复(即:如果b
为空则无效) . 数据库术语称之为inner (equi)join
.outer join 包含
a
中的元素,b
中没有相应的元素 . (即:如果b
为空,则结果均匀) . 这通常被称为left join
.A full outer join 包括来自
a
的记录以及b
,如果另一个中没有相应的元素 . (即使a
为空时甚至是结果)在RDBMS中通常看不到的是组连接[1]:
a
重复多个对应的b
元素,它将记录与相应的键分组 . 当您希望基于公共密钥枚举'joined'记录时,这通常更方便 .另见GroupJoin,其中也包含一些一般背景说明 .
[1](我相信Oracle和MSSQL都有专有扩展)
完整代码
这是一个通用的“插入式”扩展类
我决定将此作为单独的答案添加,因为我不肯定它已经足够测试了 . 这是
FullOuterJoin
方法的重新实现,主要使用LINQKit
Invoke
/Expand
的简化自定义版本Expression
,以便它可以在实体框架中运行 . 没有太多解释,因为它与我以前的答案几乎相同 .我的方法更强,但在我更好地理解之前,我发现自己从@ MichaelSander的扩展中跳出来 . 我修改它以匹配here描述的内置Enumerable.Join()方法的语法和返回类型 . 我在@ cadrell0 's comment under @JeffMercado' s解决方案中添加了"distinct"后缀 .
在示例中,您将使用它:
在将来,随着我的了解,我有一种感觉,我会转移到@ sehe的逻辑,因为它很受欢迎 . 但即使这样,我也要小心,因为我认为至少有一个重载与现有语法相匹配很重要“.Join()”方法如果可行,原因有两个:
方法的一致性有助于节省时间,避免错误并避免意外行为 .
如果将来有一个开箱即用的".FullJoin()"方法,我想它会尽量保持当前存在的".Join()"方法的语法 . 如果确实如此,那么如果要迁移到它,只需重命名函数而不更改参数或担心不同的返回类型会破坏代码 .
我仍然是泛型,扩展,Func语句和其他功能的新手,所以反馈当然是受欢迎的 .
EDIT: 我没有花很长时间才意识到我的代码存在问题 . 我在LINQPad中做了一个.Dump()并查看了返回类型 . 它只是IEnumerable,所以我试着匹配它 . 但是当我在扩展程序上实际执行了.Where()或.Select()时,我收到了一个错误:"'System Collections.IEnumerable' does not contain a definition for 'Select' and ..." . 所以最后我能够匹配.Join()的输入语法,但不能匹配返回行为 .
EDIT: 将"TResult"添加到函数的返回类型 . 错过了在阅读微软文章时,当然这是有道理的 . 有了这个修复,现在似乎返回行为完全符合我的目标 .
我真的很讨厌这些linq表达式,这就是SQL存在的原因:
在数据库中将其创建为sql视图并将其作为实体导入 .
当然,左右连接的(不同)联合也会成为它,但它是愚蠢的 .
我不知道这是否涵盖了所有情况,从逻辑上看似乎是正确的 . 我们的想法是采用左外连接和右外连接并将它们组合在一起(应该如此) .
这是写的,因为它在LINQ to Objects中 . 如果LINQ to SQL或其他,默认情况下
DefaultIfEmpty()
的重载可能无效 . 然后你必须使用条件运算符来有条件地获取值 .即,
我可能在6年前为一个应用程序编写了这个扩展类,并且在许多解决方案中一直使用它而没有问题 . 希望能帮助到你 .
我认为大多数这些问题都存在问题,包括已接受的答案,因为它们不能很好地与Linq相比IQueryable,因为服务器往返次数太多,数据返回太多,或客户端执行太多 .
对于IEnumerable我不喜欢Sehe的答案或类似因为它有过多的内存使用(一个简单的10000000两列表测试在我的32GB机器上运行Linqpad内存不足) .
此外,其他大多数实际上并没有实现正确的Full Outer Join,因为他们使用具有Right Join的Union而不是带有Right Anti Semi Join的Concat,这不仅消除了结果中重复的内部连接行,而且最初在左侧或右侧数据中存在的任何适当的重复项 .
所以这里是我的扩展,处理所有这些问题,生成SQL直接在Linq中实现连接,在服务器上执行,并且比Enumerables上的其他更快,内存更少:
Right Anti-Semi-Join之间的区别主要是Linq to Objects或源代码,但在最终答案的服务器(SQL)方面有所不同,删除了不必要的
JOIN
.使用LinqKit可以改进
Expression
处理将Expression<Func<>>
合并为lambda的手动编码,但如果语言/编译器为此添加了一些帮助,那将会很好 . 包含FullOuterJoinDistinct
和RightOuterJoin
函数是为了完整性,但我还没有重新实现FullOuterGroupJoin
.对于可以订购密钥的情况,我为
IEnumerable
编写了another version的完整外连接,这比将左外连接与右反半连接组合快约50%,至少在小集合上 . 它只排序一次后通过每个集合 .我的干净解决方案是关键在两个枚举中都是唯一的:
所以
输出: