Wie führe ich einen linken äußeren Join mit Linq-Erweiterungsmethoden durch?

272

Angenommen, ich habe eine linke äußere Verknüpfung als solche:

from f in Foo
join b in Bar on f.Foo_Id equals b.Foo_Id into g
from result in g.DefaultIfEmpty()
select new { Foo = f, Bar = result }

Wie würde ich dieselbe Aufgabe mit Erweiterungsmethoden ausdrücken? Z.B

Foo.GroupJoin(Bar, f => f.Foo_Id, b => b.Foo_Id, (f,b) => ???)
    .Select(???)
LaserJesus
quelle

Antworten:

443

Für A (links außen) verbindet eine Tabelle Barmit einem Tisch Fooauf Foo.Foo_Id = Bar.Foo_Idin Lambda Notation:

var qry = Foo.GroupJoin(
          Bar, 
          foo => foo.Foo_Id,
          bar => bar.Foo_Id,
          (x,y) => new { Foo = x, Bars = y })
       .SelectMany(
           x => x.Bars.DefaultIfEmpty(),
           (x,y) => new { Foo=x.Foo, Bar=y});
Marc Gravell
quelle
27
Das ist eigentlich nicht annähernd so verrückt, wie es scheint. Grundsätzlich GroupJoinwird die linke äußere Verbindung hergestellt, das SelectManyTeil wird nur benötigt, je nachdem, was Sie auswählen möchten.
George Mauer
6
Dieses Muster ist großartig, weil Entity Framework es als Left Join erkennt, was ich früher für unmöglich hielt
Jesan Fafon
3
@nam Nun, du brauchst eine where-Anweisung, x.Bar == null
Tod
2
@AbdulkarimKanaan ja - SelectMany glättet zwei Schichten von 1-many zu 1 Schicht mit einem Eintrag pro Paar
Marc Gravell
1
@MarcGravell Ich habe eine Bearbeitung vorgeschlagen, um ein wenig zu erklären, was Sie in Ihrer Code-Verwaltung getan haben.
B - rian
108

Da dies die De-facto-SO-Frage für Linksaußenverknüpfungen unter Verwendung der Methodensyntax (Erweiterungssyntax) zu sein scheint, dachte ich, ich würde eine Alternative zu der aktuell ausgewählten Antwort hinzufügen, die (zumindest nach meiner Erfahrung) häufiger das war, was ich bin nach dem

// Option 1: Expecting either 0 or 1 matches from the "Right"
// table (Bars in this case):
var qry = Foos.GroupJoin(
          Bars,
          foo => foo.Foo_Id,
          bar => bar.Foo_Id,
          (f,bs) => new { Foo = f, Bar = bs.SingleOrDefault() });

// Option 2: Expecting either 0 or more matches from the "Right" table
// (courtesy of currently selected answer):
var qry = Foos.GroupJoin(
                  Bars, 
                  foo => foo.Foo_Id,
                  bar => bar.Foo_Id,
                  (f,bs) => new { Foo = f, Bars = bs })
              .SelectMany(
                  fooBars => fooBars.Bars.DefaultIfEmpty(),
                  (x,y) => new { Foo = x.Foo, Bar = y });

So zeigen Sie den Unterschied mithilfe eines einfachen Datensatzes an (vorausgesetzt, wir verbinden die Werte selbst):

List<int> tableA = new List<int> { 1, 2, 3 };
List<int?> tableB = new List<int?> { 3, 4, 5 };

// Result using both Option 1 and 2. Option 1 would be a better choice
// if we didn't expect multiple matches in tableB.
{ A = 1, B = null }
{ A = 2, B = null }
{ A = 3, B = 3    }

List<int> tableA = new List<int> { 1, 2, 3 };
List<int?> tableB = new List<int?> { 3, 3, 4 };

// Result using Option 1 would be that an exception gets thrown on
// SingleOrDefault(), but if we use FirstOrDefault() instead to illustrate:
{ A = 1, B = null }
{ A = 2, B = null }
{ A = 3, B = 3    } // Misleading, we had multiple matches.
                    // Which 3 should get selected (not arbitrarily the first)?.

// Result using Option 2:
{ A = 1, B = null }
{ A = 2, B = null }
{ A = 3, B = 3    }
{ A = 3, B = 3    }    

Option 2 entspricht der typischen linken äußeren Verknüpfungsdefinition, ist jedoch, wie bereits erwähnt, je nach Datensatz häufig unnötig komplex.

Ocelot20
quelle
7
Ich denke, "bs.SingleOrDefault ()" wird nicht funktionieren, wenn Sie einen anderen folgenden Join oder Include haben. In diesem Fall benötigen wir "bs.FirstOrDefault ()".
Dherik
3
True, Entity Framework und Linq to SQL erfordern dies beide, da sie die SinglePrüfung während eines Joins nicht einfach durchführen können . SingleOrDefaultDies ist jedoch eine "korrektere" Methode, um diese IMO zu demonstrieren.
Ocelot20
1
Sie müssen daran denken, Ihre verknüpfte Tabelle zu bestellen, oder .FirstOrDefault () erhält eine zufällige Zeile aus den mehreren Zeilen, die möglicherweise den Verknüpfungskriterien entsprechen, unabhängig davon, was die Datenbank zuerst findet.
Chris Moschini
1
@ChrisMoschini: Order und FirstOrDefault sind nicht erforderlich, da das Beispiel für eine 0- oder 1-Übereinstimmung gilt, bei der Sie bei mehreren Datensätzen fehlschlagen möchten (siehe Kommentar über dem Code).
Ocelot20
2
Dies ist keine "zusätzliche Anforderung", die in der Frage nicht spezifiziert ist. Es ist das, woran viele Leute denken, wenn sie "Left Outer Join" sagen. Die FirstOrDefault-Anforderung, auf die sich Dherik bezieht, ist das EF / L2SQL-Verhalten und nicht L2Objects (keines davon befindet sich in den Tags). SingleOrDefault ist in diesem Fall absolut die richtige Methode zum Aufrufen. Natürlich möchten Sie eine Ausnahme auslösen, wenn Sie mehr Datensätze als möglich für Ihren Datensatz finden, anstatt einen beliebigen auszuwählen und zu einem verwirrenden undefinierten Ergebnis zu führen.
Ocelot20
52

Die Gruppenverbindungsmethode ist nicht erforderlich, um die Verknüpfung zweier Datensätze zu erreichen.

Inner Join:

var qry = Foos.SelectMany
            (
                foo => Bars.Where (bar => foo.Foo_id == bar.Foo_id),
                (foo, bar) => new
                    {
                    Foo = foo,
                    Bar = bar
                    }
            );

Für Left Join fügen Sie einfach DefaultIfEmpty () hinzu

var qry = Foos.SelectMany
            (
                foo => Bars.Where (bar => foo.Foo_id == bar.Foo_id).DefaultIfEmpty(),
                (foo, bar) => new
                    {
                    Foo = foo,
                    Bar = bar
                    }
            );

EF und LINQ to SQL werden korrekt in SQL umgewandelt. Für LINQ to Objects ist es besser, mit GroupJoin beizutreten, da es intern Lookup verwendet . Wenn Sie jedoch DB abfragen, ist das Überspringen von GroupJoin AFAIK als Performant.

Personlay ist für mich auf diese Weise besser lesbar als GroupJoin (). SelectMany ()

Gediminas Zimkus
quelle
Dies lief besser als ein .Join für mich, und ich konnte mein Konditonalgelenk machen, das ich wollte (rechts.FooId == links.FooId || rechts.FooId == 0)
Anders
linq2sql übersetzt diesen Ansatz als Left Join. Diese Antwort ist besser und einfacher. +1
Guido Mocha
15

Sie können eine Erweiterungsmethode erstellen wie:

public static IEnumerable<TResult> LeftOuterJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source, IEnumerable<TInner> other, Func<TSource, TKey> func, Func<TInner, TKey> innerkey, Func<TSource, TInner, TResult> res)
    {
        return from f in source
               join b in other on func.Invoke(f) equals innerkey.Invoke(b) into g
               from result in g.DefaultIfEmpty()
               select res.Invoke(f, result);
    }
Hajirazin
quelle
Das sieht so aus, als würde es funktionieren (für meine Anforderung). Können Sie ein Beispiel geben? Ich bin neu bei LINQ Extensions und habe es schwer, meinen Kopf um diese Left Join-Situation zu wickeln, in der ich mich
Shiva
@Skychan Vielleicht muss ich es mir ansehen, es ist eine alte Antwort und hat zu dieser Zeit gearbeitet. Welches Framework verwenden Sie? Ich meine .NET-Version?
Hajirazin
2
Dies funktioniert für Linq to Objects, jedoch nicht beim Abfragen einer Datenbank, da Sie auf einem IQuerable arbeiten und stattdessen Funcs-Ausdrücke verwenden müssen
Bob Vale
4

Um die Antwort von Ocelot20 zu verbessern: Wenn Sie eine Tabelle haben, bei der Sie nur noch 0 oder 1 Zeilen davon haben möchten, die jedoch mehrere Zeilen enthalten können, müssen Sie Ihre verknüpfte Tabelle bestellen:

var qry = Foos.GroupJoin(
      Bars.OrderByDescending(b => b.Id),
      foo => foo.Foo_Id,
      bar => bar.Foo_Id,
      (f, bs) => new { Foo = f, Bar = bs.FirstOrDefault() });

Andernfalls ist die Zeile, die Sie im Join erhalten, zufällig (oder genauer gesagt, je nachdem, was die Datenbank zuerst findet).

Chris Moschini
quelle
Das ist es! Jede nicht garantierte Eins-zu-Eins-Beziehung.
it3xl
2

Als ich Marc Gravells Antwort in eine Erweiterungsmethode verwandelte, machte ich Folgendes.

internal static IEnumerable<Tuple<TLeft, TRight>> LeftJoin<TLeft, TRight, TKey>(
    this IEnumerable<TLeft> left,
    IEnumerable<TRight> right,
    Func<TLeft, TKey> selectKeyLeft,
    Func<TRight, TKey> selectKeyRight,
    TRight defaultRight = default(TRight),
    IEqualityComparer<TKey> cmp = null)
{
    return left.GroupJoin(
            right,
            selectKeyLeft,
            selectKeyRight,
            (x, y) => new Tuple<TLeft, IEnumerable<TRight>>(x, y),
            cmp ?? EqualityComparer<TKey>.Default)
        .SelectMany(
            x => x.Item2.DefaultIfEmpty(defaultRight),
            (x, y) => new Tuple<TLeft, TRight>(x.Item1, y));
}
Harley Waldstein
quelle
2

Obwohl die akzeptierte Antwort funktioniert und für Linq to Objects gut ist, hat es mich gestört, dass die SQL-Abfrage nicht nur ein direkter Left Outer Join ist.

Der folgende Code basiert auf dem LinkKit-Projekt , mit dem Sie Ausdrücke übergeben und für Ihre Abfrage aufrufen können.

static IQueryable<TResult> LeftOuterJoin<TSource,TInner, TKey, TResult>(
     this IQueryable<TSource> source, 
     IQueryable<TInner> inner, 
     Expression<Func<TSource,TKey>> sourceKey, 
     Expression<Func<TInner,TKey>> innerKey, 
     Expression<Func<TSource, TInner, TResult>> result
    ) {
    return from a in source.AsExpandable()
            join b in inner on sourceKey.Invoke(a) equals innerKey.Invoke(b) into c
            from d in c.DefaultIfEmpty()
            select result.Invoke(a,d);
}

Es kann wie folgt verwendet werden

Table1.LeftOuterJoin(Table2, x => x.Key1, x => x.Key2, (x,y) => new { x,y});
Bob Vale
quelle
-1

Hierfür gibt es eine einfache Lösung

Verwenden Sie einfach .HasValue in Ihrer Auswahl

.Select(s => new 
{
    FooName = s.Foo_Id.HasValue ? s.Foo.Name : "Default Value"
}

Sehr einfach, keine Notwendigkeit für Gruppenbeiträge oder irgendetwas anderes

Dale Fraser
quelle