Como você executa uma junção externa esquerda usando métodos de extensão linq


272

Supondo que eu tenho uma junção externa esquerda como tal:

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 }

Como eu expressaria a mesma tarefa usando métodos de extensão? Por exemplo

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

Respostas:


445

Para uma (externa esquerda) juntar-se de uma tabela Barcom uma tabela Fooem Foo.Foo_Id = Bar.Foo_Idna notação 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
Na verdade, isso não é tão louco quanto parece. Basicamente GroupJoin, a junção externa esquerda, a SelectManypeça é necessária apenas dependendo do que você deseja selecionar.
21416 George Mauer

6
Este padrão é grande porque Entity Framework reconhece-lo como um Left Join, que eu costumava acreditar era uma impossibilidade
Jesan Fafon

3
@nam Bem, você precisa de um, onde declaração, x.Bar == null
Tod

2
@AbdulkarimKanaan yes - SelectMany nivela duas camadas de 1-muitas em 1 camada com uma entrada por par
Marc Gravell

1
@ MarcGravell Sugeri uma edição para adicionar um pouco de explicação sobre o que você fez em seu código de relacionamento.
precisa

109

Como essa parece ser a questão SO de fato para junções externas esquerdas usando a sintaxe do método (extensão), pensei em adicionar uma alternativa à resposta atualmente selecionada que (pelo menos em minha experiência) tem sido mais comumente o que eu sou depois de

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

Para exibir a diferença usando um conjunto de dados simples (supondo que estamos juntando os próprios valores):

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    }    

A opção 2 é verdadeira para a definição de junção externa esquerda típica, mas como mencionei anteriormente, muitas vezes é desnecessariamente complexo, dependendo do conjunto de dados.


7
Eu acho que "bs.SingleOrDefault ()" não funcionará se você tiver outro após ingressar ou incluir. Precisamos do "bs.FirstOrDefault ()" nesses casos.
Dherik

3
É verdade que o Entity Framework e o Linq to SQL exigem isso, pois não podem fazer a Singleverificação facilmente em meio a uma junção. SingleOrDefaultno entanto, é uma maneira mais "correta" de demonstrar essa IMO.
Ocelot20

1
Você precisa se lembrar de Solicitar sua tabela unida ou o .FirstOrDefault () obterá uma linha aleatória das várias linhas que podem corresponder aos critérios de junção, independentemente do que o banco de dados encontre primeiro.
Chris Moschini

1
@ ChrisMoschini: Order e FirstOrDefault são desnecessários, pois o exemplo é para uma correspondência de 0 ou 1 em que você deseja falhar em vários registros (consulte o comentário acima do código).
Ocelot20

2
Este não é um "requisito extra" não especificado na pergunta, é o que muita gente pensa quando diz "Junção externa esquerda". Além disso, o requisito FirstOrDefault referido por Dherik é o comportamento EF / L2SQL e não o L2Objects (nenhum deles está nas tags). SingleOrDefault é absolutamente o método correto para chamar neste caso. É claro que você deseja lançar uma exceção se encontrar mais registros do que o possível para o seu conjunto de dados, em vez de escolher um arbitrário e levar a um resultado indefinido confuso.
Ocelot20

52

O método Group Join é desnecessário para obter a junção de dois conjuntos de dados.

Junção interna:

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

Para a associação à esquerda, basta adicionar DefaultIfEmpty ()

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

EF e LINQ to SQL transformam corretamente em SQL. Para o LINQ to Objects, é melhor participar usando o GroupJoin, pois ele usa a Pesquisa internamente . Mas se você estiver consultando o banco de dados, ignorar o GroupJoin é o AFAIK como performant.

O Personlay para mim dessa maneira é mais legível em comparação com GroupJoin (). SelectMany ()


Este perfomed melhor do que um .join para mim, mais eu poderia fazer o meu joint conditonal que eu queria (right.FooId == left.FooId || right.FooId == 0)
Anders

linq2sql traduz essa abordagem como junção esquerda. esta resposta é melhor e mais simples. +1
Guido Mocha

15

Você pode criar um método de extensão como:

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

Parece que funcionaria (para minha exigência). você pode dar um exemplo? Eu sou novo para LINQ extensões e estou tendo dificuldade envolvendo minha cabeça em torno deste Esquerda Junte situação que eu estou em ...
Shiva

@Skychan Pode ser que eu precise olhar para ele, é uma resposta antiga e estava funcionando naquele momento. Qual framework você está usando? Quero dizer versão .NET?
hajirazin

2
Isso funciona para LINQ para objetos, mas não ao consultar um banco de dados que você precisa para operar em um IQuerable e usar expressões de Funcs vez
Bob Vale

4

Aprimorando a resposta da Ocelot20, se você tiver uma tabela com a qual você se uniu, onde você deseja apenas 0 ou 1 linhas, mas pode ter várias, você precisa ordenar a sua tabela unida:

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

Caso contrário, qual linha você obterá na junção será aleatória (ou mais especificamente, o que o banco de dados encontrar primeiro).


É isso aí! Qualquer relação um a um não garantida.
it3xl

2

Transformando a resposta de Marc Gravell em um método de extensão, fiz o seguinte.

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

Enquanto a resposta aceita funciona e é boa para o Linq to Objects, me incomodou que a consulta SQL não fosse apenas uma junção externa esquerda direta.

O código a seguir baseia-se no projeto LinkKit, que permite passar expressões e invocá-las para sua consulta.

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

Pode ser usado da seguinte maneira

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

-1

Existe uma solução fácil para isso

Basta usar .HasValue no seu Select

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

Muito fácil, sem necessidade de participar de grupos ou qualquer outra coisa

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.