SQL для выполнения объединения данных из нескольких таблиц предусматривает четыре вида объединений:
- INNER JOIN (или просто JOIN) — возвращает те (и только те) записи из двух таблиц, которые отвечают условию ON (остальные записи в выборку не попадают)
- LEFT OUTER JOIN (или просто LEFT JOIN) — возвращает все записи первой (левой) таблицы и отвечающие условию ON записи из второй (правой) таблицы; если для записи из первой таблицы нет соответствий во второй таблице, то она все равно попадет в выборку, а запись из второй таблицы будет заменена на NULL
- RIGHT OUTER JOIN (или просто RIGHT JOIN) — тоже самое, что и LEFT JOIN, но таблицы меняются местами, т.е. возвращаются все записи из второй таблицы и отвечающие условию ON записи из первой таблицы или NULL
- FULL OUTER JOIN — возвращает все записи из первой таблицы и все записи из второй таблицы, объединяя их по условию ON, при этом в обеих таблицах недостающие соотвеnствия заменяются на NULL
В LINQ все виды объединений можно выполнить с помощью выражений join
и into
в синтаксисе запросов и операторов Join()
, GroupJoin()
, DefaultIfEmpty()
и Union()
в синтаксисе методов.
INNER JOIN
INNER JOIN реализуется относительно просто, с помощью выражения join
или оператора Join()
, которые включают в выходную последовательность только те элементы левой последовательности, которым найдено соответствие в правой, остальные элементы в выборку не попадают (т.е. классический случай INNER JOIN).
В синтаксисе запросов INNER JOIN выглядит примерно так:
1 2 3 4 | var innerJoinQuery = from leftItem in LeftSequence join rightItem in RightSequence on leftItem.Id equals rightItem.leftItemId select new {leftItem = leftItem.Name, rightItem = rightItem.Name}; |
Выражение join
может объединять элементы только на основе их равенства, поэтому единственным допустимым оператором здесь является equals
.
В синтаксисе методов INNER JOIN будет выглядеть примерно так:
1 2 3 4 | var innerJoinQuery = LeftSequence.Join(RightSequence, leftItem => leftItem.Id, rightItem => rightItem.leftItemId, (leftItem, rightItem) => new {leftItem = leftItem.Name, rightItem = rightItem.Name}); |
Оператор Join()
вызывается на первой последовательности, в качестве первого аргумента принимает вторую последовательность, вторым и третьим аргументом задаются ключи, по равенству которых будут объединяться элементы, четвертый аргумент — функция, проецирующая элементы выходной последовательности.
LEFT OUTER JOIN
LEFT OUTER JOIN реализуется несколько сложнее, с помощью группирующего объединения и метода DefaultIfEmpty()
. Группирующее объединение выполняется с помощью выражения join into
в синтаксисе запросов и оператора GroupJoin()
в синтаксисе методов. Результатом группирующего объединения является иерархическая структура — последовательность последовательностей, в которой каждому элементу левой (исходной) последовательности сопоставляется последовательность элементов правой (исходной) последовательности. При этом, в отличие от обычного, не группирующего объединения, в финальную выборку включаются все элементы левой последовательности, даже если для них не найдено соответствий в правой последовательности (в этом случае элементу из левой последовательности будет сопоставлена пустая последовательность). Таким образом, у нас получается классический случай LEFT OUTER JOIN, с той лишь разницей, что результат представляет собой иерархическую структуру, а нам нужна плоская (одноуровневая) последовательность.
Поэтому вторым шагом выполняется еще одни LINQ запрос, преобразующий нашу иерархическую структуру в плоскую последовательность. Достигается это с помощью второго выражения from
или оператора SelectMany()
.
В целом в синтаксисе запросов LEFT OUTER JOIN выглядит так:
1 2 3 4 5 6 7 8 9 10 | var leftOuterJoinQuery = from leftItem in LeftSequence join rightItem in RightSequence on leftItem.Id equals rightItem.leftItemId into GroupJoin from subRightItem in GroupJoin.DefaultIfEmpty() select new { leftItem = leftItem.Name, rightItem = (subRightItem == null ? String.Empty : subRightItem.Name) }; |
Здесь сначала выполняется группирующее объединение LeftSequence
c RightSequence
по равенству полей LeftSequence.Id
и RightSequence.leftItemId
. В результате такого объединения мы получаем последовательность, состоящую из последовательностей GroupJoin
(т.е. GroupJoin
— это последовательности второго уровня в нашей сгруппированной иерархии). При этом, если для leftItem
не будет найдено совпадений в правой последовательности (RightSequence
) в сгруппированный результат для нее все равно будет добавлена последовательность GroupJoin
, но она будет пустой.
Затем с помощью второго выражения from
мы перебираем все подпоследовательности GroupJoin
и проецируем их в одноуровневую последовательность. При этом если GroupJoin
окажется пуста (соотвествий для leftItem
не было найдено в RightSequence
), оператор DefaultIfEmpty()
вернет вместо пустой последовательности последовательность с одним элементом, которому будет присвоено значение по умолчанию для данного типа (в нашем случае null
).
Оператор DefaultIfEmpty()
проверяет, что последовательность, на которой он вызван не является пустой. Если последовательность окажется пустой он возвращает последовательность с одним элементом, которому присвоено значение по умолчанию для данного типа (либо значение, переданное оператору DefaultIfEmpty()
в качестве аргумента). В противном случае, т.е. если последовательность не пуста, оператор возвращает саму последовательность.
Последние выражение в примере проецирует выходную последовательность, создавая для каждого ее элемента анонимный тип с двумя свойствами: leftItem
, которое будет равно имени элементов из левой исходной последовательности, и rightItem
, которое будет равно имени элемента из правой исходной последовательности или пустой строке, если соответствий не было найдено.
В синтаксисе методов LEFT OUTER JOIN выглядит так:
1 2 3 4 5 6 7 8 9 10 11 | var leftOuterJoinQuery = LeftSequence.GroupJoin( RightSequence, leftItem => leftItem.Id, rightItem => rightItem.leftItemId, (leftItem, rightItem ) => rightItem) .SelectMany( GroupJoin => GroupJoin.DefaultIfEmpty(), (leftItem , subRightItem ) => new { leftItem = leftItem.Name, rightItem = (subRightItem == null ? String.Empty : subRightItem.Name) }); |
RIGHT OUTER JOIN
RIGHT OUTER JOIN выполняется аналогично LEFT OUTER JOIN, просто две последовательности меняются местами.
FULL OUTER JOIN
Для выполнения FULL OUTER JOIN в LINQ не предусмотрено специальных выражений и операторов. Выполнить FULL OUTER JOIN можно в три шага:
- выполнить LEFT OUTER JOIN как описано выше
- выполнить RIGHT OUTER JOIN (т.е. фактически LEFT OUTER JOIN поменяв последовательности местами)
- соединить две полученные последовательности с помощью оператора
Union()
Оператор Union()
соединяет две последовательности (левую, на которой он вызван, и правую, переданную в качестве первого аргумента) и удаляет повторы:
1 | var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin); |