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<TResult> Join<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) {...} } 

Собственно этот метод и производит хорошо известный нам INNER JOIN. И вот теперь начинается уличная магия. Чтобы реализовать нам LEFT / RIGHT / FULL OUTER JOIN (или JOIN какой вашей душе будет угоден) необходимо подменить вызов стандартного метода на реализованый нами. Чтобы это сделать
надо переменную listA преобразовать каким то образом в тип который мы можем контролировать.

Реализовав два следующих класса:

public class JoinedEnumerable<T> : IEnumerable<T> { 	public readonly IEnumerable<T> Source; 	public bool IsOuter;  	public JoinedEnumerable(IEnumerable<T> source) { Source = source; }  	IEnumerator<T> IEnumerable<T>.GetEnumerator() { return Source.GetEnumerator(); } 	IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); } }  public static class JoinedEnumerable { 	public static JoinedEnumerable<TElement> Inner<TElement>(this IEnumerable<TElement> source) 	{ 		return Wrap(source, false); 	}  	public static JoinedEnumerable<TElement> Outer<TElement>(this IEnumerable<TElement> source) 	{ 		return Wrap(source, true); 	}  	public static JoinedEnumerable<TElement> Wrap(IEnumerable<TElement> source, bool isOuter) 	{ 		JoinedEnumerable<TElement> joinedSource  			= source as JoinedEnumerable<TElement> ??  				new JoinedEnumerable<TElement>(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<TResult> Join<TOuter, TInner, TKey, TResult>(this JoinedEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> 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<TInner>) && ((JoinedEnumerable<TInner>)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<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> 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<TResult> RightOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> 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<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> 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}; 

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

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

ссылка на оригинал статьи https://habr.com/ru/post/504854/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *