Outer Join chez LINQ

LINQ - combien a été inventé en C # juste pour que nous puissions profiter des délices de Language Integrated Query. À savoir:

  • Génériques
  • Méthodes d'extension
  • Expressions de Lamda
  • Arbres d'expression
  • Types d'Anonumus
  • Initialiseurs d'objets
  • Type inférant

Et c'est tout pour que nous puissions écrire quelque chose comme ceci:
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

On ne peut pas être en désaccord - impressionne.

Et au milieu de tout ce sucre syntaxique, il y avait une cuillère de rosée qui m'a empêché de dormir suffisamment :)

C'est un manque total de support pour OUTER JOIN. Mais comme il s'est avéré que le goudron tourne facilement ... tourne ... tourne ...

... dans un autre sucre syntaxique.

Ceux qui ont essayé de trouver une solution pour LEFT OUTER JOIN sur Internet connaissent probablement une solution similaire:

	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};
	

Une telle conception confond clairement la compréhension d'un ordre de grandeur et complique une construction déjà simple. Et ce n'est qu'un remplacement de INNER JOIN par LEFT OUTER JOIN. Afin de ne pas continuer à choquer, je ne donnerai pas d'exemple avec une FULL OUTER JOIN.

Il semblerait que ce serait simple si nous pouvions écrire comme ceci:

	var query = 
		from itemA in listA
		left join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

ou alors

	var query = 
		from itemA in listA
		full join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

Mais non. Les auteurs de C # ne nous ont pas procuré un tel plaisir. Eh bien, cela n'a pas d'importance. Néanmoins, ils nous permettront de le faire par nous-mêmes, mais pas d'une manière si belle.

Pour commencer, si quelqu'un vous dit que LINQ et l'interface System.Collections.Generic.IEnumerable ont quelque chose en commun et ne peuvent pas exister séparément, vous pouvez rire en toute sécurité en personne ...

Conception

	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

il est simplement traduit par le compilateur dans la séquence de caractères suivante:
	var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});

et peu importe le type de variables listA et listB. Supposons que listA est une variable de type TypeA, et que l'élément permalable B est de type TypeB. Ainsi, si TypeA et TypeB contiennent une propriété ou un champ nommé Key, TypeA contient la méthode Join () avec 4 arguments. Cette requête LINQ se compile librement.

Lors de l'utilisation de variables dans LINQ qui implémentent l'interface IEnumerable standard, la méthode d'extension est utilisée

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) {...}
}

En fait, cette méthode produit également le célèbre INNER JOIN. Et maintenant, la magie de la rue commence. Afin de mettre en œuvre le LEFT / RIGHT / FULL OUTER JOIN (ou JOIN qui plaira à votre âme), il est nécessaire de remplacer l'appel de la méthode standard par celui implémenté par nous. Pour ce faire, nous devons convertir la variable listA d'une manière quelconque en un type que nous pouvons contrôler.

En implémentant les deux classes suivantes:

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;
	}
}

nous pouvons facilement écrire la prochaine requête LINQ

	var query = 
		from itemA in listA.Outer()
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

et maintenant l'implémentation de la méthode d'extension Join pour la classe JoinedEnumerable de la manière dont nous avons besoin, nous obtenons tout ce dont nous avons besoin.

Et voici les méthodes d'extension:

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);
}

Voila ...

Beautiful GAUCHE OUTER JOIN:

	var query = 
		from itemA in listA.Outer()
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

Belle jointure extérieure droite:

	var query = 
		from itemA in listA.Inner()
		join itemB in listB.Outer()
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

Belle 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};

Maintenant, si vous le souhaitez, vous pouvez utiliser votre propre approche - car le champ de l'imagination est énorme ici. J'ai plusieurs solutions plus intéressantes pour implémenter des goodies dans la zashashnik. Nous aurons le temps de les partager.

Merci pour l'attention.

Que POWER soit avec vous!

All Articles