Поведение
Предположим, у вас есть два списка:
Id Value
1 A
2 B
3 C
Id ChildValue
1 a1
1 a2
1 a3
2 b1
2 b2
Когда вы Join
два списка на Id
поле, результат будет:
Value ChildValue
A a1
A a2
A a3
B b1
B b2
Когда вы GroupJoin
два списка на Id
поле, результат будет:
Value ChildValues
A [a1, a2, a3]
B [b1, b2]
C []
Так Join
выдает плоский (табличный) результат родительских и дочерних значений.
GroupJoin
создает список записей в первом списке, каждый из которых содержит группу соединенных записей во втором списке.
Вот почему Join
эквивалент INNER JOIN
в SQL: нет записей для C
. Хотя GroupJoin
эквивалент эквивалентен OUTER JOIN
: C
находится в наборе результатов, но с пустым списком связанных записей (в наборе результатов SQL будет строка C - null
).
Синтаксис
Так что пусть два списка будут IEnumerable<Parent>
и IEnumerable<Child>
соответственно. (В случае Linq to Entities:) IQueryable<T>
.
Join
синтаксис будет
from p in Parent
join c in Child on p.Id equals c.Id
select new { p.Value, c.ChildValue }
возвращая IEnumerable<X>
где X является анонимным типом с двумя свойствами, Value
и ChildValue
. Этот синтаксис запроса использует Join
метод под капотом.
GroupJoin
синтаксис будет
from p in Parent
join c in Child on p.Id equals c.Id into g
select new { Parent = p, Children = g }
возвращая IEnumerable<Y>
где Y - анонимный тип, состоящий из одного свойства типа Parent
и свойства типа IEnumerable<Child>
. Этот синтаксис запроса использует GroupJoin
метод под капотом.
Мы могли бы просто сделать select g
в последнем запросе, который выберет IEnumerable<IEnumerable<Child>>
, скажем, список списков. Во многих случаях выбор с включенным родителем более полезен.
Некоторые варианты использования
1. Изготовление плоского внешнего соединения.
Как сказано в заявлении ...
from p in Parent
join c in Child on p.Id equals c.Id into g
select new { Parent = p, Children = g }
... производит список родителей с дочерними группами. Это можно превратить в плоский список пар родитель-потомок двумя небольшими дополнениями:
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 }
Результат похож на
Value Child
A a1
A a2
A a3
B b1
B b2
C (null)
Обратите внимание, что переменная диапазона c
повторно используется в приведенном выше утверждении. Делая это, любой join
оператор может быть просто преобразован в оператор outer join
путем добавления эквивалента into g from c in g.DefaultIfEmpty()
в существующий join
оператор.
Это где запрос (или полный) синтаксис сияет. Синтаксис метода (или свободный) показывает, что на самом деле происходит, но трудно написать:
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 } )
Таким образом, квартира outer join
в LINQ является GroupJoin
плоской SelectMany
.
2. Сохранение порядка
Предположим, что список родителей немного длиннее. Некоторый пользовательский интерфейс создает список выбранных родителей в виде Id
значений в фиксированном порядке. Давайте использовать:
var ids = new[] { 3,7,2,4 };
Теперь выбранные родители должны быть отфильтрованы из списка родителей в этом точном порядке.
Если мы сделаем ...
var result = parents.Where(p => ids.Contains(p.Id));
... порядок parents
будет определять результат. Если родители заказаны Id
, результатом будут родители 2, 3, 4, 7. Не хорошо. Тем не менее, мы также можем использовать join
для фильтрации списка. И используя в ids
качестве первого списка, порядок будет сохранен:
from id in ids
join p in parents on id equals p.Id
select p
В результате родители 3, 7, 2, 4.