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

时间:2009-02-25 05:35:04

标签: c# linq-to-sql lambda

假设我有一个左外连接:

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 }

如何使用扩展方法表达相同的任务? E.g。

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

8 个答案:

答案 0 :(得分:390)

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

答案 1 :(得分:89)

由于这似乎是使用方法(扩展)语法的左外连接的事实上的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对于典型的左外连接定义是正确的,但正如我之前提到的,根据数据集的不同,通常会不必要地复杂。

答案 2 :(得分:37)

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

内部加入:

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

对于Left Join,只需添加DefaultIfEmpty()

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

EF正确转换为SQL。 对于LINQ to对象,使用GroupJoin加入它是更好的,因为它在内部使用Lookup,但是如果你要查询DB,那么跳过GroupJoin就是AFAIK的性能。

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

答案 3 :(得分:14)

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

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

答案 4 :(得分: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() });

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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

以下代码依赖于LinkKit 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});

答案 7 :(得分:0)

对此有一个简单的解决方案

只需在您的Select中使用.HasValue

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

非常容易,不需要groupjoin或其他任何东西