LINQ 扩展连接






4.95/5 (171投票s)
System.Linq 中缺失的连接
引言
我第一次对 LinQ 产生兴趣是在 2007 年,除了对其强大的功能和无限的可能性感到钦佩之外,我当时还非常怀念某些东西,作为一名拥有“广泛”知识的 SQL 开发人员,很难理解对象查询的新理念。这仅仅是因为 LinQ 基础类扩展方法中缺少许多 SQL 的高级连接语句。
起初,我以为这只是微软开发团队的问题,会在后续版本中得到解决。但随着我对产品分析的深入,我意识到这可能不是面向对象开发者的最佳工作方式,并且需要抛弃为数据库专业人士提供的解决方案,因为它们对我们来说可能并不那么有效。当我了解 Entity Framework 的属性时,这一点就更加突出了。
直到今天,我才明白世上没有绝对的事,关键在于实用性。有些解决方案可能不完全符合纯粹的开发原则,但它们非常方便,能够节省时间和金钱,这对于一名出色的开发者来说至关重要。我们可以在 Framework 中找到一些例子(如扩展方法、匿名类型、动态类型等)。
背景
几年前,我读了 C. L. Moffatt 的一篇文章(链接),他非常清晰简洁地解释了 SQL 中的各种连接类型,并且我一直以来都在思考是否要写一篇类似的关于 LinQ 的文章。现在我决定这么做了。
我在论坛上看到过很多关于这个话题的问答,但我找不到一个能够涵盖所有内容的。我打算在接下来的内容中弥补这些不足。
本文不仅是一篇教学文章,还希望能通过一个示例项目让其他人的生活更轻松,其中应用了文章中提到的所有概念。此外,还提供了一个扩展类,对于那些不想花费太多时间编写每个概念代码的人来说会很有用。
安装
LinQ 扩展连接是一个开源项目,您的代码可在 Github 上找到。
您的安装非常简单,我们提供了一个 nuget 包。
在类中添加 using MoralesLarios.Linq
即可使用。
更新代码
我在扩展方法中添加了一个泛型约束
where TSource : class where TInner : class
通过这个约束,我修复了使用不可为空序列的 bug。
Using the Code
我将使用两个类来演示我的示例
public class Person
{
public string ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public double Salary { get; set; }
public DateTime Born { get; set; }
public int IdAddress { get; set; }
}
public class Address
{
public int IdAddress { get; set; }
public string Street { get; set; }
public int Num { get; set; }
public string City { get; set; }
}
这是 Person
类的默认值
这是 Address
类的默认值
我的扩展方法库有六个扩展方法。主要方法 INNER JOIN
已在 linq 基础库中实现。接下来的方法将进行解释
- 内连接(Inner Join)
- 左连接
- 右连接
- 全外部联接
- 排除内部连接的左连接
- 排除内部连接的右连接
- 排除内部连接的完全外连接
内连接(Inner Join)
这是主方法。它已在 .NET Framework 中实现,因此没有它的扩展方法。
var result = from p in Person.BuiltPersons()
join a in Address.BuiltAddresses()
on p.IdAddress equals a.IdAddress
select new
{
Name = a.MyPerson.Name,
Age = a.MyPerson.Age,
PersonIdAddress = a.MyPerson.IdAddress,
AddressIdAddress = a.MyAddress.IdAddress,
Street = a.MyAddress.Street
};
Lambda 表达式
var resultJoint = Person.BuiltPersons().Join( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = a.MyPerson.Name,
Age = a.MyPerson.Age,
PersonIdAddress = a.MyPerson.IdAddress,
AddressIdAddress = a.MyAddress.IdAddress,
Street = a.MyAddress.Street
});
正如我们所见,扩展方法有五个主要部分,这些部分将在其余扩展方法中共享
- 是主集合
- 是内部集合
- 是主键
- 是外键
- 是结果集合的类型
先前查询的结果
正如我们所见,PersonIdAddresses
的值与 AddressIdAddresses
匹配。
左连接
扩展方法
public static IEnumerable<TResult>
LeftJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source,
IEnumerable<TInner> inner,
Func<TSource, TKey> pk,
Func<TInner, TKey> fk,
Func<TSource, TInner, TResult> result)
where TSource : class where TInner : class
{
IEnumerable<TResult> _result = Enumerable.Empty<TResult>();
_result = from s in source
join i in inner
on pk(s) equals fk(i) into joinData
from left in joinData.DefaultIfEmpty()
select result(s, left);
return _result;
}
Lambda 表达式
var resultJoint = Person.BuiltPersons().LeftJoin( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = a.MyPerson.Name,
Age = a.MyPerson.Age,
PersonIdAddress = a.MyPerson.IdAddress,
AddressIdAddress = (a.MyAddress != null ? a.MyAddress.IdAddress : -1),
Street = (a.MyAddress != null ? a.MyAddress.Street : "Null-Value")
});
在这里,我们在调用 select
方法并构建我们新的结果类型时,必须注意,我们必须控制 Address
类返回的值,因为返回的对象可能是 null
,在这种情况下,读取其任何属性都会引发 NullReferenceException
。
先前查询的结果
右连接
扩展方法
public static IEnumerable<TResult>
RightJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source,
IEnumerable<TInner> inner,
Func<TSource, TKey> pk,
Func<TInner, TKey> fk,
Func<TSource, TInner, TResult> result)
where TSource : class where TInner : class
{
IEnumerable<TResult> _result = Enumerable.Empty<TResult>();
_result = from i in inner
join s in source
on fk(i) equals pk(s) into joinData
from right in joinData.DefaultIfEmpty()
select result(right, i);
return _result;
}
Lambda 表达式
var resultJoint = Person.BuiltPersons().RightJoin( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = (a.MyPerson != null ? a.MyPerson.Name : "Null-Value"),
Age = (a.MyPerson != null ? a.MyPerson.Age : -1),
PersonIdAddress = (a.MyPerson != null ? a.MyPerson.IdAddress : -1),
AddressIdAddress = a.MyAddress.IdAddress,
Street = a.MyAddress.Street
});
请注意,为了避免异常,我们必须控制 Person
类中的 null
值。
先前查询的结果
全外部联接
扩展方法
public static IEnumerable<TResult>
FullOuterJoinJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source,
IEnumerable<TInner> inner,
Func<TSource, TKey> pk,
Func<TInner, TKey> fk,
Func<TSource, TInner, TResult> result)
where TSource : class where TInner : class
{
var left = source.LeftJoin(inner, pk, fk, result).ToList();
var right = source.RightJoin(inner, pk, fk, result).ToList();
return left.Union(right);
}
Lambda 表达式
var resultJoint = Person.BuiltPersons().FullOuterJoinJoin( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = (a.MyPerson != null ?
a.MyPerson.Name : "Null-Value"),
Age = (a.MyPerson != null ?
a.MyPerson.Age : -1),
PersonIdAddress = (a.MyPerson != null ?
a.MyPerson.IdAddress : -1),
AddressIdAddress = (a.MyAddress != null ?
a.MyAddress.IdAddress : -1),
Street = (a.MyAddress != null ?
a.MyAddress.Street : "Null-Value")
});
请注意,我们必须同时控制两个类中的 null
值。
先前查询的结果
左排除连接
扩展方法
public static IEnumerable<TResult>
LeftExcludingJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source,
IEnumerable<TInner> inner,
Func<TSource, TKey> pk,
Func<TInner, TKey> fk,
Func<TSource, TInner, TResult> result)
where TSource : class where TInner : class
{
IEnumerable<TResult> _result = Enumerable.Empty<TResult>();
_result = from s in source
join i in inner
on pk(s) equals fk(i) into joinData
from left in joinData.DefaultIfEmpty()
where left == null
select result(s, left);
return _result;
}
Lambda 表达式
var resultJoint = Person.BuiltPersons().LeftExcludingJoin( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = a.MyPerson.Name,
Age = a.MyPerson.Age,
PersonIdAddress = a.MyPerson.IdAddress,
AddressIdAddress = (a.MyAddress != null ? a.MyAddress.IdAddress : -1),
Street = (a.MyAddress != null ?
a.MyAddress.Street : "Null-Value")
});
请注意,我们必须控制 Address
类中的 null
值。
先前查询的结果
右排除连接
扩展方法
public static IEnumerable<TResult>
RightExcludingJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source,
IEnumerable<TInner> inner,
Func<TSource, TKey> pk,
Func<TInner, TKey> fk,
Func<TSource, TInner, TResult> result)
where TSource : class where TInner : class
{
IEnumerable<TResult> _result = Enumerable.Empty<TResult>();
_result = from i in inner
join s in source
on fk(i) equals pk(s) into joinData
from right in joinData.DefaultIfEmpty()
where right == null
select result(right, i);
return _result;
}
Lambda 表达式
var resultJoint = Person.BuiltPersons().RightExcludingJoin( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = (a.MyPerson != null ?
a.MyPerson.Name : "Null-Value"),
Age = (a.MyPerson != null ? a.MyPerson.Age : -1),
PersonIdAddress = (a.MyPerson != null ? a.MyPerson.IdAddress : -1),
AddressIdAddress = a.MyAddress.IdAddress,
Street = a.MyAddress.Street
});
请注意,我们必须控制 Person
类中的 null
值。
先前查询的结果
完全外排除连接
扩展方法
public static IEnumerable<TResult>
FulltExcludingJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source,
IEnumerable<TInner> inner,
Func<TSource, TKey> pk,
Func<TInner, TKey> fk,
Func<TSource, TInner, TResult> result)
where TSource : class where TInner : class
{
var left = source.LeftExcludingJoin(inner, pk, fk, result).ToList();
var right = source.RightExcludingJoin(inner, pk, fk, result).ToList();
return left.Union(right);
}
Lambda 表达式
var resultJoint = Person.BuiltPersons().FulltExcludingJoin( /// Source Collection
Address.BuiltAddresses(), /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
.Select(a => new
{
Name = (a.MyPerson != null ?
a.MyPerson.Name : "Null-Value"),
Age = (a.MyPerson != null ?
a.MyPerson.Age : -1),
PersonIdAddress = (a.MyPerson != null ?
a.MyPerson.IdAddress : -1),
AddressIdAddress = (a.MyAddress != null ?
a.MyAddress.IdAddress : -1),
Street = (a.MyAddress != null ?
a.MyAddress.Street : "Null-Value")
});
请注意,我们必须同时控制两个类中的 null
值。
先前查询的结果
-- 最佳解决方案
我认为这是 OOP 开发者的最佳解决方案。
var GroupPersons = this.Persons.GroupJoin(this.Addresses, /// Inner Collection
p => p.IdAddress, /// PK
a => a.IdAddress, /// FK
(p, a) => /// Result Collection
new {
MyPerson = p,
Addresses = a.Select(ad => ad).ToList()
}).ToList();
或
var GroupAddresses = this.Addresses.GroupJoin(this.Persons, /// Inner Collection
a => a.IdAddress, /// PK
p => p.IdAddress, /// FK
(a, p) => /// Result Collection
new {
MyAddress = a,
Persons = p.Select(ps => ps).ToList()
}).ToList();
用于填充 treeview
的代码
foreach (var data in GroupPersons)
{
TreeViewItem tbi = new TreeViewItem{ Header = data.MyPerson };
this.treePersons.Items.Add(tbi);
foreach (var d in data.Addresses)
{
TreeViewItem tbiChild =
new TreeViewItem { Header = d , Background = Brushes.Gainsboro };
this.treePersons.Items.OfType<TreeViewItem>().Last().Items.Add(tbiChild);
}
}
或
foreach (var data in GroupAddresses)
{
TreeViewItem tbi = new TreeViewItem{ Header = data.MyAddress };
this.treeAddresses.Items.Add(tbi);
foreach (var d in data.Persons)
{
TreeViewItem tbiChild =
new TreeViewItem { Header = d , Background = Brushes.Gainsboro };
this.treeAddresses.Items.OfType<TreeViewItem>().Last().Items.Add(tbiChild);
}
}
结果
我们更改了 IdAddress
的值,为了更清晰地看到,我们必须这样做。
结果
应用程序测试
在测试应用程序中,我们可以更改 Person
和 Address
集合的值,并选择要应用的 join
,更改将应用于结果集合。
谢谢
感谢 Santiago Sánchez 和 Cesar Sanz 为他们的英语。
历史
- 2012 年 11 月 4 日:初始版本