Comportamento
Suponha que você tenha duas listas:
Id Value
1 A
2 B
3 C
Id ChildValue
1 a1
1 a2
1 a3
2 b1
2 b2
Quando você Join
as duas listas no Id
campo, o resultado será:
Value ChildValue
A a1
A a2
A a3
B b1
B b2
Quando você GroupJoin
as duas listas no Id
campo, o resultado será:
Value ChildValues
A [a1, a2, a3]
B [b1, b2]
C []
Portanto, Join
produz um resultado simples (tabular) dos valores pai e filho.
GroupJoin
produz uma lista de entradas na primeira lista, cada uma com um grupo de entradas unidas na segunda lista.
É por isso que Join
é equivalente INNER JOIN
no SQL: não há entradas para C
. Enquanto GroupJoin
é o equivalente a OUTER JOIN
: C
está no conjunto de resultados, mas com uma lista vazia de entradas relacionadas (em um conjunto de resultados SQL, haveria uma linha C - null
).
Sintaxe
Então deixe as duas listas serem IEnumerable<Parent>
e IEnumerable<Child>
respectivamente. (No caso de Linq to Entities:) IQueryable<T>
.
Join
sintaxe seria
from p in Parent
join c in Child on p.Id equals c.Id
select new { p.Value, c.ChildValue }
retornando um IEnumerable<X>
onde X é um tipo anônimo com duas propriedades Value
e ChildValue
. Essa sintaxe de consulta usa o Join
método sob o capô.
GroupJoin
sintaxe seria
from p in Parent
join c in Child on p.Id equals c.Id into g
select new { Parent = p, Children = g }
retornando um IEnumerable<Y>
onde Y é um tipo anônimo que consiste em uma propriedade do tipo Parent
e uma propriedade do tipo IEnumerable<Child>
. Essa sintaxe de consulta usa o GroupJoin
método sob o capô.
Poderíamos fazer select g
na última consulta, que selecionaria uma IEnumerable<IEnumerable<Child>>
, digamos uma lista de listas. Em muitos casos, a seleção com o pai incluído é mais útil.
Alguns casos de uso
1. Produzindo uma junção externa plana.
Como disse, a declaração ...
from p in Parent
join c in Child on p.Id equals c.Id into g
select new { Parent = p, Children = g }
... produz uma lista de pais com grupos de crianças. Isso pode ser transformado em uma lista simples de pares pai-filho por duas pequenas adições:
from p in parents
join c in children on p.Id equals c.Id into g // <= into
from c in g.DefaultIfEmpty() // <= flattens the groups
select new { Parent = p.Value, Child = c?.ChildValue }
O resultado é semelhante ao
Value Child
A a1
A a2
A a3
B b1
B b2
C (null)
Observe que a variável range c
é reutilizada na instrução acima. Fazendo isso, qualquer join
instrução pode simplesmente ser convertida em uma outer join
adicionando o equivalente into g from c in g.DefaultIfEmpty()
a uma join
instrução existente .
É aqui que a sintaxe da consulta (ou abrangente) brilha. A sintaxe do método (ou fluente) mostra o que realmente acontece, mas é difícil escrever:
parents.GroupJoin(children, p => p.Id, c => c.Id, (p, c) => new { p, c })
.SelectMany(x => x.c.DefaultIfEmpty(), (x,c) => new { x.p.Value, c?.ChildValue } )
Portanto, um apartamento outer join
no LINQ é um GroupJoin
, achatado por SelectMany
.
2. Preservando a ordem
Suponha que a lista de pais seja um pouco mais longa. Algumas UI produzem uma lista de pais selecionados como Id
valores em uma ordem fixa. Vamos usar:
var ids = new[] { 3,7,2,4 };
Agora os pais selecionados devem ser filtrados da lista de pais nesta ordem exata.
Se nós fizermos ...
var result = parents.Where(p => ids.Contains(p.Id));
... a ordem de parents
determinará o resultado. Se os pais são ordenados por Id
, o resultado será os pais 2, 3, 4, 7. Não é bom. No entanto, também podemos usar join
para filtrar a lista. E, usando ids
como primeira lista, o pedido será preservado:
from id in ids
join p in parents on id equals p.Id
select p
O resultado são os pais 3, 7, 2, 4.