【问题标题】:How do you perform a left outer join using linq extension methods如何使用 linq 扩展方法执行左外连接
【发布时间】:2010-10-09 17:56:30
【问题描述】:

假设我有这样的左外连接:

from f in Foo
join b in Bar on f.Foo_Id equals b.Foo_Id into g
from result in g.DefaultIfEmpty()
select new { Foo = f, Bar = result }

如何使用扩展方法表达相同的任务?例如

Foo.GroupJoin(Bar, f => f.Foo_Id, b => b.Foo_Id, (f,b) => ???)
    .Select(???)

【问题讨论】:

    标签: c# linq-to-sql lambda


    【解决方案1】:

    对于表 Bar 与表 Foo 的(左外)连接,Foo.Foo_Id = Bar.Foo_Id 采用 lambda 表示法:

    var qry = Foo.GroupJoin(
              Bar, 
              foo => foo.Foo_Id,
              bar => bar.Foo_Id,
              (x,y) => new { Foo = x, Bars = y })
           .SelectMany(
               x => x.Bars.DefaultIfEmpty(),
               (x,y) => new { Foo=x.Foo, Bar=y});
    

    【讨论】:

    • 这实际上并不像看起来那么疯狂。基本上GroupJoin 做左外连接,SelectMany 部分只需要取决于您要选择的内容。
    • 这种模式很棒,因为 Entity Framework 将其识别为左连接,我曾经认为这是不可能的
    • @MarcGravell 选择only 右侧列全部为空的行(匹配不满足时SQL Server Outer Join 中的情况),您将如何实现相同的效果?
    • @nam 那么你需要一个 where 语句,x.Bar == null
    • @AbdulkarimKanaan 是的 - SelectMany 将两层 1-many 扁平化为 1 层,每对有一个条目
    【解决方案2】:

    由于这似乎是使用方法(扩展)语法的左外连接事实上的 SO 问题,我想我会为当前选择的答案添加一个替代方案,(至少在我的经验中)更常见的是我在追求

    // Option 1: Expecting either 0 or 1 matches from the "Right"
    // table (Bars in this case):
    var qry = Foos.GroupJoin(
              Bars,
              foo => foo.Foo_Id,
              bar => bar.Foo_Id,
              (f,bs) => new { Foo = f, Bar = bs.SingleOrDefault() });
    
    // Option 2: Expecting either 0 or more matches from the "Right" table
    // (courtesy of currently selected answer):
    var qry = Foos.GroupJoin(
                      Bars, 
                      foo => foo.Foo_Id,
                      bar => bar.Foo_Id,
                      (f,bs) => new { Foo = f, Bars = bs })
                  .SelectMany(
                      fooBars => fooBars.Bars.DefaultIfEmpty(),
                      (x,y) => new { Foo = x.Foo, Bar = y });
    

    使用简单的数据集显示差异(假设我们正在加入值本身):

    List<int> tableA = new List<int> { 1, 2, 3 };
    List<int?> tableB = new List<int?> { 3, 4, 5 };
    
    // Result using both Option 1 and 2. Option 1 would be a better choice
    // if we didn't expect multiple matches in tableB.
    { A = 1, B = null }
    { A = 2, B = null }
    { A = 3, B = 3    }
    
    List<int> tableA = new List<int> { 1, 2, 3 };
    List<int?> tableB = new List<int?> { 3, 3, 4 };
    
    // Result using Option 1 would be that an exception gets thrown on
    // SingleOrDefault(), but if we use FirstOrDefault() instead to illustrate:
    { A = 1, B = null }
    { A = 2, B = null }
    { A = 3, B = 3    } // Misleading, we had multiple matches.
                        // Which 3 should get selected (not arbitrarily the first)?.
    
    // Result using Option 2:
    { A = 1, B = null }
    { A = 2, B = null }
    { A = 3, B = 3    }
    { A = 3, B = 3    }    
    

    选项 2 适用于典型的左外连接定义,但正如我之前提到的,根据数据集的不同,它通常过于复杂。

    【讨论】:

    • 我认为如果您有另一个后续加入或包含,“bs.SingleOrDefault()”将不起作用。在这种情况下,我们需要“bs.FirstOrDefault()”。
    • 是的,Entity Framework 和 Linq to SQL 都要求这样做,因为它们不能在连接中轻松地执行 Single 检查。 SingleOrDefault 然而是一种更“正确”的方式来展示这个 IMO。
    • 您需要记住对您的连接表进行排序,否则 .FirstOrDefault() 将从可能与连接条件匹配的多行中获取随机行,无论数据库首先找到什么。
    • @ChrisMoschini:Order 和 FirstOrDefault 是不必要的,因为该示例适用于 0 或 1 匹配,您希望在多条记录上失败(请参阅上面的代码注释)。
    • 这不是问题中未指定的“额外要求”,这是很多人在说“Left Outer Join”时所想到的。此外,Dherik 提到的 FirstOrDefault 要求是 EF/L2SQL 行为,而不是 L2Objects(这些都不在标签中)。 SingleOrDefault 绝对是在这种情况下调用的正确方法。当然,如果您遇到的记录多于数据集的可能记录,您当然希望抛出异常,而不是选择任意一条并导致令人困惑的未定义结果。
    【解决方案3】:

    不需要Group Join方法来实现两个数据集的连接。

    内连接:

    var qry = Foos.SelectMany
                (
                    foo => Bars.Where (bar => foo.Foo_id == bar.Foo_id),
                    (foo, bar) => new
                        {
                        Foo = foo,
                        Bar = bar
                        }
                );
    

    对于左连接只需添加 DefaultIfEmpty()

    var qry = Foos.SelectMany
                (
                    foo => Bars.Where (bar => foo.Foo_id == bar.Foo_id).DefaultIfEmpty(),
                    (foo, bar) => new
                        {
                        Foo = foo,
                        Bar = bar
                        }
                );
    

    EF 和 LINQ to SQL 正确转换为 SQL。 对于 LINQ to Objects,最好使用 GroupJoin 加入,因为它在内部使用 Lookup。但是,如果您正在查询数据库,那么跳过 GroupJoin 是 AFAIK 的表现。

    与 GroupJoin().SelectMany() 相比,这种方式的 Personlay 对我来说更具可读性

    【讨论】:

    • 这对我来说比 .Join 表现得更好,而且我可以做我想要的条件连接 (right.FooId == left.FooId || right.FooId == 0)
    • linq2sql 将此方法翻译为左连接。这个答案更好更简单。 +1
    • 警告!将我的查询从 GroupJoin 更改为这种方法会导致 CROSS OUTER APPLY 而不是 LEFT OUTER JOIN。根据您的查询,这可能会导致非常不同的性能。 (使用 EF Core 5)
    【解决方案4】:

    您可以创建如下扩展方法:

    public static IEnumerable<TResult> LeftOuterJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source, IEnumerable<TInner> other, Func<TSource, TKey> func, Func<TInner, TKey> innerkey, Func<TSource, TInner, TResult> res)
        {
            return from f in source
                   join b in other on func.Invoke(f) equals innerkey.Invoke(b) into g
                   from result in g.DefaultIfEmpty()
                   select res.Invoke(f, result);
        }
    

    【讨论】:

    • 这看起来可行(满足我的要求)。你能举个例子吗?我是 LINQ 扩展的新手,我很难理解我所处的这种左连接情况......
    • @Skychan 可能是我需要看看它,这是旧的答案,当时正在工作。您使用的是哪个框架?我的意思是 .NET 版本?
    • 这适用于 Linq to Objects,但不适用于查询数据库,因为您需要对 IQuerable 进行操作并改用函数表达式
    【解决方案5】:

    改进 Ocelot20 的回答,如果你有一个表,你只需要在其中加入 0 或 1 行,但它可能有多个,你需要订购你的加入表:

    var qry = Foos.GroupJoin(
          Bars.OrderByDescending(b => b.Id),
          foo => foo.Foo_Id,
          bar => bar.Foo_Id,
          (f, bs) => new { Foo = f, Bar = bs.FirstOrDefault() });
    

    否则,您在联接中获得的哪一行将是随机的(或者更具体地说,无论 db 碰巧先找到哪一行)。

    【讨论】:

    • 就是这样!任何无保证的一对一关系。
    【解决方案6】:

    虽然接受的答案有效并且对 Linq to Objects 有好处,但它让我烦恼的是 SQL 查询不仅仅是一个直接的左外连接。

    以下代码依赖于LinqKit Project,它允许您传递表达式并将它们调用到您的查询中。

    static IQueryable<TResult> LeftOuterJoin<TSource,TInner, TKey, TResult>(
         this IQueryable<TSource> source, 
         IQueryable<TInner> inner, 
         Expression<Func<TSource,TKey>> sourceKey, 
         Expression<Func<TInner,TKey>> innerKey, 
         Expression<Func<TSource, TInner, TResult>> result
        ) {
        return from a in source.AsExpandable()
                join b in inner on sourceKey.Invoke(a) equals innerKey.Invoke(b) into c
                from d in c.DefaultIfEmpty()
                select result.Invoke(a,d);
    }
    

    可以如下使用

    Table1.LeftOuterJoin(Table2, x => x.Key1, x => x.Key2, (x,y) => new { x,y});
    

    【讨论】:

    • LinkKit 应该拼写为 LinqKit,因为它在 GitHub/NuGet 上。 LinkKit 看起来完全不同。 @Bob Vale 我无法直接编辑您的帖子,因为 SO 不允许单个字母编辑。
    【解决方案7】:

    将 Marc Gravell 的答案转换为扩展方法,我做了以下操作。

    internal static IEnumerable<Tuple<TLeft, TRight>> LeftJoin<TLeft, TRight, TKey>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> selectKeyLeft,
        Func<TRight, TKey> selectKeyRight,
        TRight defaultRight = default(TRight),
        IEqualityComparer<TKey> cmp = null)
    {
        return left.GroupJoin(
                right,
                selectKeyLeft,
                selectKeyRight,
                (x, y) => new Tuple<TLeft, IEnumerable<TRight>>(x, y),
                cmp ?? EqualityComparer<TKey>.Default)
            .SelectMany(
                x => x.Item2.DefaultIfEmpty(defaultRight),
                (x, y) => new Tuple<TLeft, TRight>(x.Item1, y));
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-08
      • 2014-03-04
      • 1970-01-01
      • 2013-05-26
      相关资源
      最近更新 更多