65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ 扩展连接

2012 年 11 月 4 日

CPOL

5分钟阅读

viewsIcon

248325

downloadIcon

4632

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 基础库中实现。接下来的方法将进行解释

  1. 内连接(Inner Join)
  2. 左连接
  3. 右连接
  4. 全外部联接
  5. 排除内部连接的左连接
  6. 排除内部连接的右连接
  7. 排除内部连接的完全外连接

内连接(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
                    });  

正如我们所见,扩展方法有五个主要部分,这些部分将在其余扩展方法中共享

  1. 是主集合
  2. 是内部集合
  3. 是主键
  4. 是外键
  5. 是结果集合的类型

先前查询的结果

正如我们所见,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 的值,为了更清晰地看到,我们必须这样做

结果

应用程序测试

在测试应用程序中,我们可以更改 PersonAddress 集合的值,并选择要应用的 join,更改将应用于结果集合。

谢谢

感谢 Santiago Sánchez 和 Cesar Sanz 为他们的英语。

历史

  • 2012 年 11 月 4 日:初始版本
© . All rights reserved.