Поведение
Предположим, у вас есть два списка:
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.