[Из песочницы] Outer Join в LINQ
LINQ — как много было придумано в C# лишь для того чтобы мы могли наслаждаться прелестями Language Integrated Query. А именно:
- Generics
- Extension Methods
- Lamda expressions
- Expression trees
- Anonumus types
- Object initializers
- Type inferring
И это все чтобы мы могли написать нечто вроде такого:
var query =
from itemA in listA
join itemB in listB
on itemA.Key equals itemB.Key
select new {itemA, itemB};
Нельзя не согласиться — впечталяет.
И среди всего этого синтаксического сахара была ложка дегдя которая мне не давала нормально выспаться :)
Это тотальное отсутствие поддержки OUTER JOIN. Но как оказалось деготь с легкостью превращается… превращается… превращается…
… в еще один «синтаксический сахар».
Тем, кто пытался найти решение для LEFT OUTER JOIN в интернете, наверняка знакомо подобное решение:
var query =
from itemA in listA
join itemB in listB
on itemA.Key equals itemB.Key into outer
from itemO in outer.DefaultIfEmpty()
select new {itemA, itemO};
Подобная конструкция явно на порядок запутывает понимание и усложняет и без того простую конструкцию. А это лишь замена INNER JOIN на LEFT OUTER JOIN. Чтобы не продолжать шокировать, пример с FULL OUTER JOIN приводить не буду.
Казалось бы как было бы просто если бы могли написать вот так:
var query =
from itemA in listA
left join itemB in listB
on itemA.Key equals itemB.Key
select new {itemA, itemB};
или так
var query =
from itemA in listA
full join itemB in listB
on itemA.Key equals itemB.Key
select new {itemA, itemB};
Ан нет. Авторы C# нам такого удовольствия не предоставили. Ну не беда. Все же они позволят нам это сделать самостоятельно, хоть и не таким красивым способом.
Начнем с того, что если кто то вам скажет, что LINQ и интерфейс System.Collections.Generic.IEnumerable имеют что то общее и не могут существовать по отдельности можете смело рассмеяться в лицо…
Конструкция
var query =
from itemA in listA
join itemB in listB
on itemA.Key equals itemB.Key
select new {itemA, itemB};
просто напросто транслируется компилятором в следующую последовательность символов:
var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});
и абсолютно не важно какого типа переменные listA и listB. Предположим что listA переменная типа TypeA, а пермеменная itemB типа TypeB. Так вот, если TypeA и TypeB содеражат свойство или поле с именем Key, TypeA содержит метод Join () с 4 мя аргументами. Этот LINQ запрос свободно откомпилируется.
При использовании в LINQ переменных которые реализуют стандартный интерфейс IEnumerable используется метод расширения
public class System.Linq.Enumerable
{
public static IEnumerable Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector) {...}
}
Собственно этот метод и производит хорошо известный нам INNER JOIN. И вот теперь начинается уличная магия. Чтобы реализовать нам LEFT / RIGHT / FULL OUTER JOIN (или JOIN какой вашей душе будет угоден) необходимо подменить вызов стандартного метода на реализованый нами. Чтобы это сделать
надо переменную listA преобразовать каким то образом в тип который мы можем контролировать.
Реализовав два следующих класса:
public class JoinedEnumerable : IEnumerable
{
public readonly IEnumerable Source;
public bool IsOuter;
public JoinedEnumerable(IEnumerable source) { Source = source; }
IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); }
}
public static class JoinedEnumerable
{
public static JoinedEnumerable Inner(this IEnumerable source)
{
return Wrap(source, false);
}
public static JoinedEnumerable Outer(this IEnumerable source)
{
return Wrap(source, true);
}
public static JoinedEnumerable Wrap(IEnumerable source, bool isOuter)
{
JoinedEnumerable joinedSource
= source as JoinedEnumerable ??
new JoinedEnumerable(source);
joinedSource.IsOuter = isOuter;
return joinedSource;
}
}
мы запросто пишем следующий LINQ запрос
var query =
from itemA in listA.Outer()
join itemB in listB
on itemA.Key equals itemB.Key
select new {itemA, itemB};
и теперь реализовав метод расширения Join для класса JoinedEnumerable нужным нам образом получаем все что нам нужно.
А вот собственно и методы расширения:
public static IEnumerable Join(this JoinedEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer comparer = null)
{
if (outer == null) throw new ArgumentNullException("outer");
if (inner == null) throw new ArgumentNullException("inner");
if (outerKeySelector == null) throw new ArgumentNullException("outerKeySelector");
if (innerKeySelector == null) throw new ArgumentNullException("innerKeySelector");
if (resultSelector == null) throw new ArgumentNullException("resultSelector");
bool leftOuter = outer.IsOuter;
bool rightOuter = (inner is JoinedEnumerable) && ((JoinedEnumerable)inner).IsOuter;
if (leftOuter && rightOuter)
return FullOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);
if (leftOuter)
return LeftOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);
if (rightOuter)
return RightOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);
return Enumerable.Join(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);
}
public static IEnumerable LeftOuterJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer comparer = null)
{
var innerLookup = inner.ToLookup(innerKeySelector, comparer);
foreach (var outerItem in outer)
foreach (var innerItem in innerLookup[outerKeySelector(outerItem)].DefaultIfEmpty())
yield return resultSelector(outerItem, innerItem);
}
public static IEnumerable RightOuterJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer comparer = null)
{
var outerLookup = outer.ToLookup(outerKeySelector, comparer);
foreach (var innerItem in inner)
foreach (var outerItem in outerLookup[innerKeySelector(innerItem)].DefaultIfEmpty())
yield return resultSelector(outerItem, innerItem);
}
public static IEnumerable FullOuterJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func resultSelector, IEqualityComparer comparer = null)
{
var outerLookup = outer.ToLookup(outerKeySelector, comparer);
var innerLookup = inner.ToLookup(innerKeySelector, comparer);
foreach (var innerGrouping in innerLookup)
if (!outerLookup.Contains(innerGrouping.Key))
foreach (TInner innerItem in innerGrouping)
yield return resultSelector(default(TOuter), innerItem);
foreach (var outerGrouping in outerLookup)
foreach (var innerItem in innerLookup[outerGrouping.Key].DefaultIfEmpty())
foreach (var outerItem in outerGrouping)
yield return resultSelector(outerItem, innerItem);
}
Вуаля…
Красивый LEFT OUTER JOIN:
var query =
from itemA in listA.Outer()
join itemB in listB
on itemA.Key equals itemB.Key
select new {itemA, itemB};
Красивый RIGHT OUTER JOIN:
var query =
from itemA in listA.Inner()
join itemB in listB.Outer()
on itemA.Key equals itemB.Key
select new {itemA, itemB};
Красивый FULL OUTER JOIN:
var query =
from itemA in listA.Outer()
join itemB in listB.Outer()
on itemA.Key equals itemB.Key
select new {itemA, itemB};
Теперь при желании вы можете использовать и свой подход — так как поле для фантазии здесь громаднейшее. У меня в загашнике есть еще несколько инетерсных решений для реализации вкусностей. Будет время обязательно поделюсь ими.
Спасибо за внимание.
Да пребудет с вами СИЛА!