Outer Join bei LINQ

LINQ - wie viel wurde in C # erfunden, damit wir die Freuden der sprachintegrierten Abfrage genießen können. Nämlich:

  • Generika
  • Erweiterungsmethoden
  • Lamda-Ausdrücke
  • Ausdrucksbäume
  • Anonumus-Typen
  • Objektinitialisierer
  • Typ ableiten

Und das ist alles, damit wir so etwas schreiben können:
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

Man kann nicht widersprechen - beeindruckt.

Und inmitten all dieses syntaktischen Zuckers gab es einen Löffel Tau, der mich daran hinderte, genug Schlaf zu bekommen :)

Dies ist ein völliger Mangel an Unterstützung für OUTER JOIN. Aber wie sich herausstellte, dreht sich Teer leicht ... dreht sich ... dreht sich ...

... in einen anderen syntaktischen Zucker.

Diejenigen, die versucht haben, im Internet eine Lösung für LEFT OUTER JOIN zu finden, kennen wahrscheinlich eine ähnliche Lösung:

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

Ein solches Design verwirrt das Verständnis eindeutig um eine Größenordnung und erschwert eine bereits einfache Konstruktion. Und dies ist nur ein Ersatz für INNER JOIN durch LEFT OUTER JOIN. Um nicht weiter zu schockieren, werde ich kein Beispiel mit einem FULL OUTER JOIN geben.

Es scheint einfach zu sein, wenn wir so schreiben könnten:

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

oder so

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

Aber nein. Die Autoren von C # haben uns nicht so viel Freude bereitet. Nun, das spielt keine Rolle. Trotzdem werden sie uns erlauben, dies alleine zu tun, wenn auch nicht auf so schöne Weise.

Wenn Ihnen jemand sagt, dass LINQ und die System.Collections.Generic.IEnumerable-Schnittstelle etwas gemeinsam haben und nicht separat existieren können, können Sie zunächst sicher persönlich lachen ...

Design

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

Es wird vom Compiler einfach in die folgende Zeichenfolge übersetzt:
	var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});

und es spielt keine Rolle, welche Art von Variablen listA und listB sind. Angenommen, listA ist eine Variable vom Typ TypeA und das permalable itemB ist vom Typ TypeB. Wenn TypeA und TypeB eine Eigenschaft oder ein Feld mit dem Namen Key enthalten, enthält TypeA die Join () -Methode mit 4 Argumenten. Diese LINQ-Abfrage wird frei kompiliert.

Bei der Verwendung von Variablen in LINQ, die die standardmäßige IEnumerable-Schnittstelle implementieren, wird die Erweiterungsmethode verwendet

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

Tatsächlich erzeugt diese Methode auch den bekannten INNER JOIN. Und jetzt beginnt die Straßenmagie. Um den LEFT / RIGHT / FULL OUTER JOIN (oder JOIN, der Ihrer Seele gefällt) zu implementieren, muss der Aufruf der Standardmethode durch den von uns implementierten ersetzt werden. Dazu müssen wir die Variable listA auf irgendeine Weise in einen Typ konvertieren, den wir steuern können.

Durch Implementierung der folgenden zwei Klassen:

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

Wir können leicht die nächste LINQ-Abfrage schreiben

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

Wenn wir nun die Join-Erweiterungsmethode für die JoinedEnumerable-Klasse so implementieren, wie wir es benötigen, erhalten wir alles, was wir brauchen.

Und hier sind die Erweiterungsmethoden:

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 ...

Wunderschöner LEFT OUTER JOIN:

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

Schöne 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};

Schöne VOLL OUTER JOIN:

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

Wenn Sie möchten, können Sie jetzt Ihren eigenen Ansatz verwenden - da das Feld für Vorstellungskraft hier enorm ist. Ich habe einige weitere interessante Lösungen für die Implementierung von Goodies in der Zashashnik. Es wird Zeit sein, sie zu teilen.

Vielen Dank für Ihre Aufmerksamkeit.

Möge POWER mit dir sein!

All Articles