НОВОСТИ [Из песочницы] Outer Join в LINQ

Alvaros
Онлайн
Регистрация
14.05.16
Сообщения
21.452
Реакции
101
Репутация
204
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};



Теперь при желании вы можете использовать и свой подход — так как поле для фантазии здесь громаднейшее. У меня в загашнике есть еще несколько инетерсных решений для реализации вкусностей. Будет время обязательно поделюсь ими.

Спасибо за внимание.
Да прибудет с вами СИЛА!
 
Сверху Снизу