如何使用linq扩展方法执行左外部联接


272

假设我有一个这样的左外部联接:

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(???)

Answers:


444

对于一个(左外)加入一个表的Bar一个表FooFoo.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});

27
实际上,这并没有看上去那么疯狂。基本上GroupJoin是左外部连接,SelectMany仅根据要选择的部分才需要该零件。
George Mauer 2014年

6
这种模式是伟大的,因为实体框架将其识别为一个左连接,这在我以前认为是不可能的
Jesan Fafon

3
@nam好吧,您需要一个where语句,x.Bar == null
Tod

2
@AbdulkarimKanaan是的-SelectMany将两对一的多对一扁平化为1层,每对有一个条目
Marc Gravell

1
@MarcGravell我建议进行编辑,以对您在代码段中所做的事情添加一些解释。
B–rian

109

由于这似乎是使用方法(扩展名)语法的左外部联接的实际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适用于典型的左外部联接定义,但是正如我前面提到的,根据数据集,它通常不必要地复杂。


7
我认为“ bs.SingleOrDefault()”将在您加入另一个“加入”或“包含”之后不起作用。在这种情况下,我们需要“ bs.FirstOrDefault()”。
Dherik

3
的确,实体框架和Linq to SQL都要求这样做,因为它们在联接中不容易进行Single检查。SingleOrDefault但是,这是演示此IMO的更“正确”的方法。
Ocelot20

1
您需要记住对联接的表进行排序,否则无论数据库碰巧首先找到了什么,.FirstOrDefault()都会从可能符合联接条件的多个行中获取随机行。
克里斯·莫斯基尼

1
@ChrisMoschini:订单和FirstOrDefault是不必要的,因为该示例用于0或1匹配,您可能希望在多个记录上失败(请参见上面的代码注释)。
Ocelot20

2
这不是问题中未指定的“额外要求”,而是很多人在说“外部加入”时的想法。同样,Dherik引用的FirstOrDefault要求是EF / L2SQL行为,而不是L2Objects(这些都不在标记中)。在这种情况下,SingleOrDefault绝对是正确的方法。当然,如果遇到的记录多于数据集的记录数,那么您想引发一个异常,而不是选择一个任意记录并导致令人困惑的未定义结果。
Ocelot20

52

组联接方法对于实现两个数据集的联接是不必要的。

内部联接:

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)
Anders

linq2sql将这种方法转换为左联接。这个答案更好,更简单。+1
Guido Mocha

15

您可以创建扩展方法,例如:

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 Extensions的新手,我很难解决这种左连接情况,我处于...
Shiva 2014年

@Skychan可能是我需要查看的内容,它是旧答案,并且当时正在工作。您正在使用哪个框架?我的意思是.NET版本?
hajirazin

2
这对Linq to Objects有效,但在查询数据库时无效,因为您需要对IQuerable进行操作并改为使用Funcs表达式
Bob Vale,

4

改进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() });

否则,您从联接中获得的行将是随机的(或更具体地说,无论哪个数据库首先找到)。


而已!任何无担保的一对一关系。
it3xl

2

将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));
}

2

虽然可接受的答案有效,并且对Linq to Objects有利,但它使我感到困惑,即SQL查询不仅仅是直接的Left Outer Join。

以下代码依赖于LinkKit项目,该项目允许您传递表达式并将其调用到查询中。

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});

-1

有一个简单的解决方案

只需在您的选择中使用.HasValue

.Select(s => new 
{
    FooName = s.Foo_Id.HasValue ? s.Foo.Name : "Default Value"
}

非常简单,不需要groupjoin或其他任何东西

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.