LINQ - Full Outer Join


202

Ich habe eine Liste der Personenausweise und ihres Vornamens sowie eine Liste der Personenausweise und ihres Nachnamens. Einige Leute haben keinen Vornamen und andere keinen Nachnamen. Ich möchte einen vollständigen äußeren Join für die beiden Listen durchführen.

Also die folgenden Listen:

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

Sollte produzieren:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

Ich bin neu bei LINQ (also vergib mir, wenn ich lahm bin) und habe einige Lösungen für 'LINQ Outer Joins' gefunden, die alle ziemlich ähnlich aussehen, aber wirklich äußere Joins zu sein scheinen.

Meine bisherigen Versuche gehen ungefähr so:

private void OuterJoinTest()
{
    List<FirstName> firstNames = new List<FirstName>();
    firstNames.Add(new FirstName { ID = 1, Name = "John" });
    firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

    List<LastName> lastNames = new List<LastName>();
    lastNames.Add(new LastName { ID = 1, Name = "Doe" });
    lastNames.Add(new LastName { ID = 3, Name = "Smith" });

    var outerJoin = from first in firstNames
        join last in lastNames
        on first.ID equals last.ID
        into temp
        from last in temp.DefaultIfEmpty()
        select new
        {
            id = first != null ? first.ID : last.ID,
            firstname = first != null ? first.Name : string.Empty,
            surname = last != null ? last.Name : string.Empty
        };
    }
}

public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}

Aber das kehrt zurück:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

Was mache ich falsch?


2
Benötigen Sie dies, um nur für In-Memory-Listen oder für Linq2Sql zu arbeiten?
JamesFaix

Antworten:


122

Ich weiß nicht, ob dies alle Fälle abdeckt, logischerweise scheint es richtig zu sein. Die Idee ist, eine linke äußere Verbindung und eine rechte äußere Verbindung zu nehmen und dann die Vereinigung der Ergebnisse vorzunehmen.

var firstNames = new[]
{
    new { ID = 1, Name = "John" },
    new { ID = 2, Name = "Sue" },
};
var lastNames = new[]
{
    new { ID = 1, Name = "Doe" },
    new { ID = 3, Name = "Smith" },
};
var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last?.Name,
    };
var rightOuterJoin =
    from last in lastNames
    join first in firstNames on last.ID equals first.ID into temp
    from first in temp.DefaultIfEmpty()
    select new
    {
        last.ID,
        FirstName = first?.Name,
        LastName = last.Name,
    };
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

Dies funktioniert wie geschrieben, da es sich in LINQ to Objects befindet. Bei LINQ to SQL oder anderen unterstützt der Abfrageprozessor möglicherweise keine sichere Navigation oder andere Vorgänge. Sie müssten den bedingten Operator verwenden, um die Werte bedingt abzurufen.

dh

var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last != null ? last.Name : default,
    };

2
Union wird Duplikate beseitigen. Wenn Sie keine Duplikate erwarten oder die zweite Abfrage schreiben können, um alles auszuschließen, was in der ersten enthalten war, verwenden Sie stattdessen Concat. Dies ist der SQL-Unterschied zwischen UNION und UNION ALL
cadrell0

3
@ cadre110 Duplikate treten auf, wenn eine Person einen Vor- und einen Nachnamen hat, sodass Union eine gültige Wahl ist.
Saus

1
@saus, aber es gibt eine ID-Spalte. Selbst wenn es einen doppelten Vor- und Nachnamen gibt, sollte die ID unterschiedlich sein
cadrell0

1
Ihre Lösung funktioniert für primitive Typen, scheint jedoch nicht für Objekte zu funktionieren. In meinem Fall ist Vorname ein Domänenobjekt, während Nachname ein anderes Domänenobjekt ist. Wenn ich die beiden Ergebnisse vereinige, hat LINQ eine NotSupportedException ausgelöst (Typen in Union oder Concat sind inkompatibel aufgebaut). Haben Sie ähnliche Probleme gehabt?
Candy Chiu

1
@ CandyChiu: Ich bin eigentlich nie auf einen solchen Fall gestoßen. Ich denke, das ist eine Einschränkung bei Ihrem Abfrageanbieter. In diesem Fall möchten Sie wahrscheinlich LINQ to Objects verwenden, indem Sie aufrufen, AsEnumerable()bevor Sie die Vereinigung / Verkettung durchführen. Versuchen Sie das und sehen Sie, wie das geht. Wenn dies nicht der Weg ist, den Sie gehen möchten, bin ich mir nicht sicher, ob ich Ihnen weiterhelfen kann.
Jeff Mercado

196

Update 1: Bereitstellung einer wirklich verallgemeinerten Erweiterungsmethode FullOuterJoin
Update 2: Optionales Akzeptieren einer benutzerdefinierten IEqualityComparerMethode für den Schlüsseltyp
Update 3 : Diese Implementierung wurde kürzlich Teil vonMoreLinq - Danke Jungs!

Bearbeiten hinzugefügt FullOuterGroupJoin( ideone ). Ich habe die GetOuter<>Implementierung wiederverwendet , wodurch sie um einen Bruchteil weniger leistungsfähig ist, als es sein könnte, aber ich strebe derzeit einen Code auf hoher Ebene an, der nicht auf dem neuesten Stand ist.

Sehen Sie es live auf http://ideone.com/O36nWc

static void Main(string[] args)
{
    var ax = new[] { 
        new { id = 1, name = "John" },
        new { id = 2, name = "Sue" } };
    var bx = new[] { 
        new { id = 1, surname = "Doe" },
        new { id = 3, surname = "Smith" } };

    ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b})
        .ToList().ForEach(Console.WriteLine);
}

Druckt die Ausgabe:

{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b =  }
{ a = , b = { id = 3, surname = Smith } }

Sie können auch Standardeinstellungen angeben: http://ideone.com/kG4kqO

    ax.FullOuterJoin(
            bx, a => a.id, b => b.id, 
            (a, b, id) => new { a.name, b.surname },
            new { id = -1, name    = "(no firstname)" },
            new { id = -2, surname = "(no surname)" }
        )

Drucken:

{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }

Erklärung der verwendeten Begriffe:

Joining ist ein Begriff, der aus dem relationalen Datenbankdesign entlehnt wurde:

  • Ein Join wiederholt Elemente aso oft, wie Elemente b mit dem entsprechenden Schlüssel vorhanden sind (dh: nichts, wenn bleer). Datenbankjargon nennt diesinner (equi)join .
  • Eine äußere Verknüpfung enthält Elemente, afür die kein entsprechendes Element vorhanden ist b. (dh: sogar Ergebnisse, wenn bleer). Dies wird üblicherweise als bezeichnetleft join .
  • Ein vollständiger äußerer Join enthält Datensätze von a sowie,b wenn im anderen kein entsprechendes Element vorhanden ist. (dh sogar Ergebnisse, wenn aleer waren)

Etwas, das in RDBMS normalerweise nicht zu sehen ist, ist ein Gruppenbeitritt [1] :

  • Ein Gruppen-Join macht dasselbe wie oben beschrieben, aber anstatt Elemente von afür mehrere entsprechende zu wiederholen b, gruppiert er die Datensätze mit entsprechenden Schlüsseln. Dies ist häufig praktischer, wenn Sie anhand eines gemeinsamen Schlüssels durch "verbundene" Datensätze auflisten möchten.

Siehe auch GroupJoin, das auch einige allgemeine Hintergrunderklärungen enthält.


[1] (Ich glaube, Oracle und MSSQL haben dafür proprietäre Erweiterungen.)

Vollständiger Code

Eine verallgemeinerte 'Drop-In'-Erweiterungsklasse dafür

internal static class MyExtensions
{
    internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection,
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   let xa = alookup[key]
                   let xb = blookup[key]
                   select projection(xa, xb, key);

        return join;
    }

    internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<TA, TB, TKey, TResult> projection,
        TA defaultA = default(TA), 
        TB defaultB = default(TB),
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   from xa in alookup[key].DefaultIfEmpty(defaultA)
                   from xb in blookup[key].DefaultIfEmpty(defaultB)
                   select projection(xa, xb, key);

        return join;
    }
}

Bearbeitet, um die Verwendung der FullOuterJoinbereitgestellten Erweiterungsmethode zu zeigen
siehe

Bearbeitet: FullOuterGroupJoin Erweiterungsmethode hinzugefügt
sehe

4
Anstatt ein Wörterbuch zu verwenden, können Sie eine Suche verwenden , die die in Ihren Hilfserweiterungsmethoden ausgedrückten Funktionen enthält. Zum Beispiel können Sie a.GroupBy(selectKeyA).ToDictionary();als a.ToLookup(selectKeyA)und adict.OuterGet(key)als schreiben alookup[key]. Das Abrufen der Schlüsselsammlung ist jedoch etwas schwieriger : alookup.Select(x => x.Keys).
Riskanter Martin

1
@RiskyMartin Danke! Das macht das Ganze in der Tat eleganter. Ich habe die Antwort und die Ideen aktualisiert . (Ich nehme an, die Leistung sollte erhöht werden, da weniger Objekte instanziiert werden).
sehe

1
@Revious funktioniert nur, wenn Sie wissen, dass die Schlüssel eindeutig sind. Und das ist nicht der übliche Fall für / grouping /. Davon abgesehen auf jeden Fall. Wenn Sie wissen, dass der Hash perf nicht schleppen wird (knotenbasierte Container haben im Prinzip höhere Kosten und Hashing ist nicht kostenlos und die Effizienz hängt von der Hash-Funktion / Bucket-Spread ab), ist er sicherlich algorithmisch effizienter. Also, für kleine Lasten würde ich erwarten, dass es nicht schneller sein könnte
siehe

27

Ich denke, es gibt Probleme mit den meisten dieser Probleme, einschließlich der akzeptierten Antwort, da sie mit Linq über IQueryable nicht gut funktionieren, entweder weil zu viele Server-Roundtrips und zu viele Daten zurückgegeben werden oder weil zu viele Clients ausgeführt werden.

Für IEnumerable mag ich Sehes Antwort oder ähnliches nicht, weil sie übermäßig viel Speicher benötigt (ein einfacher 10000000-Test mit zwei Listen führte dazu, dass Linqpad auf meinem 32-GB-Computer nicht mehr über genügend Speicher verfügte).

Außerdem implementieren die meisten anderen keine ordnungsgemäße vollständige äußere Verknüpfung, da sie eine Union mit einer rechten Verknüpfung anstelle von Concat mit einer rechten Antisemi-Verknüpfung verwenden, wodurch nicht nur die doppelten inneren Verknüpfungszeilen aus dem Ergebnis entfernt werden, sondern auch Alle richtigen Duplikate, die ursprünglich in den linken oder rechten Daten vorhanden waren.

Hier sind meine Erweiterungen, die all diese Probleme behandeln, SQL generieren sowie den Join in LINQ zu SQL direkt implementieren, auf dem Server ausgeführt werden und schneller und mit weniger Speicher als andere in Enumerables ausgeführt werden:

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLgR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

Der Unterschied zwischen einem rechten Antisemi-Join ist meistens bei Linq to Objects oder in der Quelle umstritten, macht jedoch in der endgültigen Antwort einen Unterschied auf der Serverseite (SQL), wodurch unnötige entfernt werden JOIN.

Die Handcodierung für Expressiondas Zusammenführen eines Expression<Func<>>zu einem Lambda könnte mit LinqKit verbessert werden, aber es wäre schön, wenn die Sprache / der Compiler eine Hilfe dafür hinzugefügt hätte. Die FullOuterJoinDistinctund RightOuterJoinFunktionen sind der Vollständigkeit halber enthalten, aber ich habe sie noch nicht neu implementiert FullOuterGroupJoin.

Ich habe eine andere Version eines vollständigen äußeren Joins für IEnumerableFälle geschrieben, in denen der Schlüssel bestellbar ist. Dies ist etwa 50% schneller als das Kombinieren des linken äußeren Joins mit dem rechten Anti-Semi-Join, zumindest bei kleinen Sammlungen. Es geht durch jede Sammlung nach nur einmaligem Sortieren.

Ich habe auch eine weitere Antwort für eine Version hinzugefügt , die mit EF funktioniert, indem ich die Invokedurch eine benutzerdefinierte Erweiterung ersetzt habe.


Was ist TP unusedP, TC unusedClos? Sind sie buchstäblich unbenutzt?
Rudey

Ja, sie sind nur vorhanden , um die Arten in zu erfassen TP, TC, um TResultdie richtigen zu erstellen Expression<Func<>>. Ich sollte ich sie mit ersetzen könnte _, __, ___statt, aber das scheint nicht klarer bis C # einen richtigen Parameter Wildcard hat stattdessen zu verwenden.
NetMage

1
@MarcL. Ich bin mir bei "lästig" nicht so sicher - aber ich stimme zu, dass diese Antwort in diesem Zusammenhang sehr nützlich ist. Beeindruckendes Zeug (obwohl es für mich die Mängel von Linq-to-SQL bestätigt)
sehe

3
Ich bekomme The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Gibt es irgendwelche Einschränkungen mit diesem Code? Ich möchte einen vollständigen Beitritt über IQueryables durchführen
Learner

1
Ich habe eine neue Antwort fügte hinzu , dass ersetzt Invokemit einem individuell gestalteten ExpressionVisitordie Inline Invokeso dass es mit EF funktionieren sollte. Kannst du es versuchen?
NetMage

7

Hier ist eine Erweiterungsmethode, die dies tut:

public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector)
{
    var leftOuterJoin = from left in leftItems
        join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp
        from right in temp.DefaultIfEmpty()
        select new { left, right };

    var rightOuterJoin = from right in rightItems
        join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp
        from left in temp.DefaultIfEmpty()
        select new { left, right };

    var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

    return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right));
}

3
+1. R ⟗ S = (R ⟕ S) ∪ (R ⟖ S), was eine vollständige äußere Verbindung bedeutet = linke äußere Verbindung alle rechten äußeren Verbindung! Ich schätze die Einfachheit dieses Ansatzes.
TamusJRoyce

1
@TamusJRoyce Außer Unionentfernt Duplikate. Wenn die Originaldaten also doppelte Zeilen enthalten, werden diese nicht im Ergebnis angezeigt .
NetMage

Toller Punkt! Fügen Sie eine eindeutige ID hinzu, wenn Sie verhindern möchten, dass Duplikate entfernt werden. Ja. Die Vereinigung ist etwas verschwenderisch, es sei denn, Sie können darauf hinweisen, dass es eine eindeutige ID gibt und die Vereinigung wechselt zu Vereinigung alle (über interne Heuristiken / Optimierungen). Aber es wird funktionieren.
TamusJRoyce

Gleich wie die akzeptierte Antwort .
Gert Arnold

7

Ich vermute, dass der Ansatz von @ sehe stärker ist, aber bis ich es besser verstehe, bin ich von der Erweiterung von @ MichaelSander abgesprungen. Ich habe es geändert, um es an die Syntax und den Rückgabetyp der hier beschriebenen integrierten Enumerable.Join () -Methode anzupassen . Ich habe das "eindeutige" Suffix in Bezug auf den Kommentar von @ cadrell0 unter der Lösung von @ JeffMercado angehängt.

public static class MyExtensions {

    public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> (
        this IEnumerable<TLeft> leftItems, 
        IEnumerable<TRight> rightItems, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector
    ) {

        var leftJoin = 
            from left in leftItems
            join right in rightItems 
              on leftKeySelector(left) equals rightKeySelector(right) into temp
            from right in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        var rightJoin = 
            from right in rightItems
            join left in leftItems 
              on rightKeySelector(right) equals leftKeySelector(left) into temp
            from left in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        return leftJoin.Union(rightJoin);
    }

}

Im Beispiel würden Sie es folgendermaßen verwenden:

var test = 
    firstNames
    .FullJoinDistinct(
        lastNames,
        f=> f.ID,
        j=> j.ID,
        (f,j)=> new {
            ID = f == null ? j.ID : f.ID, 
            leftName = f == null ? null : f.Name,
            rightName = j == null ? null : j.Name
        }
    );

Wenn ich in Zukunft mehr lerne, habe ich das Gefühl, dass ich angesichts der Popularität auf die Logik von @ sehe migrieren werde. Aber selbst dann muss ich vorsichtig sein, da ich es aus zwei Gründen für wichtig halte, mindestens eine Überladung zu haben, die der Syntax der vorhandenen ".Join ()" - Methode entspricht, wenn dies machbar ist:

  1. Konsistenz der Methoden hilft, Zeit zu sparen, Fehler zu vermeiden und unbeabsichtigtes Verhalten zu vermeiden.
  2. Sollte es in Zukunft jemals eine sofort einsatzbereite ".FullJoin ()" - Methode geben, würde ich mir vorstellen, dass sie versuchen wird, die Syntax der derzeit vorhandenen ".Join ()" - Methode beizubehalten, wenn dies möglich ist. Wenn dies der Fall ist, können Sie Ihre Funktionen einfach umbenennen, ohne die Parameter zu ändern oder sich Gedanken über verschiedene Rückgabetypen zu machen, die Ihren Code beschädigen.

Ich bin noch neu mit Generika, Erweiterungen, Func-Anweisungen und anderen Funktionen, daher ist Feedback auf jeden Fall willkommen.

EDIT: Ich habe nicht lange gebraucht, um festzustellen, dass ein Problem mit meinem Code vorliegt. Ich habe einen .Dump () in LINQPad ausgeführt und mir den Rückgabetyp angesehen. Es war nur IEnumerable, also habe ich versucht, es abzugleichen. Aber als ich tatsächlich ein .Where () oder .Select () für meine Erweiterung ausgeführt habe, wurde eine Fehlermeldung angezeigt: "'System Collections.IEnumerable' enthält keine Definition für 'Select' und ...". Am Ende konnte ich also die Eingabesyntax von .Join () anpassen, aber nicht das Rückgabeverhalten.

EDIT: Added „TResult“ der Rückgabetyp für die Funktion. Ich habe das beim Lesen des Microsoft-Artikels verpasst und es macht natürlich Sinn. Mit diesem Fix scheint das Rückkehrverhalten nun doch meinen Zielen zu entsprechen.


+2 für diese Antwort sowie Michael Sanders. Ich habe dies versehentlich angeklickt und die Abstimmung ist gesperrt. Bitte fügen Sie zwei hinzu.
TamusJRoyce

@ TamusJRoyce, ich habe gerade die Codeformate ein wenig bearbeitet. Ich glaube, nachdem eine Änderung vorgenommen wurde, haben Sie die Möglichkeit, Ihre Stimme neu zu formulieren. Probieren Sie es aus, wenn Sie möchten.
Pwilcox

Ich danke dir sehr!
Roshna Omer

6

Wie Sie festgestellt haben, verfügt Linq nicht über ein Konstrukt "Outer Join". Der nächste, den Sie erhalten können, ist ein linker äußerer Join mit der von Ihnen angegebenen Abfrage. Dazu können Sie alle Elemente der Nachnamenliste hinzufügen, die nicht im Join enthalten sind:

outerJoin = outerJoin.Concat(lastNames.Select(l=>new
                            {
                                id = l.ID,
                                firstname = String.Empty,
                                surname = l.Name
                            }).Where(l=>!outerJoin.Any(o=>o.id == l.id)));

2

Ich mag die Antwort von sehe, aber es wird keine verzögerte Ausführung verwendet (die Eingabesequenzen werden durch die Aufrufe von ToLookup eifrig aufgezählt). Nachdem ich mir die .NET-Quellen für LINQ-to-Objects angesehen hatte , kam ich zu folgendem Ergebnis :

public static class LinqExtensions
{
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator = null,
        TLeft defaultLeft = default(TLeft),
        TRight defaultRight = default(TRight))
    {
        if (left == null) throw new ArgumentNullException("left");
        if (right == null) throw new ArgumentNullException("right");
        if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector");
        if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");

        comparator = comparator ?? EqualityComparer<TKey>.Default;
        return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight);
    }

    internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator,
        TLeft defaultLeft,
        TRight defaultRight)
    {
        var leftLookup = left.ToLookup(leftKeySelector, comparator);
        var rightLookup = right.ToLookup(rightKeySelector, comparator);
        var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator);

        foreach (var key in keys)
            foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft))
                foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight))
                    yield return resultSelector(leftValue, rightValue, key);
    }
}

Diese Implementierung hat die folgenden wichtigen Eigenschaften:

  • Bei verzögerter Ausführung werden Eingabesequenzen nicht aufgelistet, bevor die Ausgabesequenz aufgezählt wird.
  • Zählt die Eingabesequenzen jeweils nur einmal auf.
  • Erhält die Reihenfolge der Eingabesequenzen in dem Sinne, dass Tupel in der Reihenfolge der linken und dann der rechten Sequenz (für die Tasten, die in der linken Sequenz nicht vorhanden sind) ausgegeben werden.

Diese Eigenschaften sind wichtig, da sie das sind, was jemand erwartet, der neu bei FullOuterJoin ist, aber Erfahrung mit LINQ hat.


Die Reihenfolge der Eingabesequenzen wird nicht beibehalten: Lookup garantiert dies nicht. Daher werden diese Foreaches in einer bestimmten Reihenfolge auf der linken Seite und in einer bestimmten Reihenfolge auf der linken Seite aufgelistet. Die relationale Reihenfolge der Elemente bleibt jedoch nicht erhalten.
Ivan Danilov

@IvanDanilov Sie haben Recht, dass dies nicht im Vertrag enthalten ist. Die Implementierung von ToLookup verwendet jedoch eine interne Lookup-Klasse in Enumerable.cs, die Gruppierungen in einer verknüpften Liste mit Einfügungsreihenfolge aufbewahrt und diese Liste verwendet, um sie zu durchlaufen. In der aktuellen .NET-Version ist die Reihenfolge garantiert, aber da MS dies leider nicht dokumentiert hat, können sie es in späteren Versionen ändern.
Søren Boisen

Ich habe es unter .NET 4.5.1 unter Win 8.1 versucht, und die Reihenfolge bleibt nicht erhalten.
Ivan Danilov

1
"..die Eingabesequenzen werden durch die Aufrufe von ToLookup eifrig aufgezählt". Aber Ihre Implementierung macht genau das Gleiche. Das Nachgeben gibt hier aufgrund der Kosten für die Finite-State-Maschine nicht viel.
pkuderov

4
Die Suchaufrufe werden ausgeführt, wenn das erste Element des Ergebnisses angefordert wird und nicht, wenn der Iterator erstellt wird. Das bedeutet verzögerte Ausführung. Sie können die Aufzählung eines Eingabesatzes noch weiter verschieben, indem Sie die linke Aufzählung direkt iterieren, anstatt sie in eine Suche umzuwandeln. Dies führt zu dem zusätzlichen Vorteil, dass die Reihenfolge der linken Gruppe erhalten bleibt.
Rolf

2

Ich habe beschlossen, dies als separate Antwort hinzuzufügen, da ich nicht sicher bin, dass es ausreichend getestet wurde. Dies ist eine Neuimplementierung der FullOuterJoinMethode unter Verwendung einer vereinfachten, angepassten Version von LINQKit Invoke/ Expandfor, Expressiondamit sie mit dem Entity Framework funktioniert. Es gibt nicht viel Erklärung, da es so ziemlich das gleiche ist wie meine vorherige Antwort.

public static class Ext {
    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lrg,r) => resultSelector(lrg.left, r)
        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lrg");
        var parmC = Expression.Parameter(typeof(TRight), "r");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(resultSelector.Apply(argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lgr,l) => resultSelector(l, lgr.right)
        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lgr");
        var parmC = Expression.Parameter(typeof(TLeft), "l");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(resultSelector.Apply(parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right })
                         .SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    private static Expression<Func<TParm, TResult>> CastSBody<TParm, TResult>(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression<Func<TParm, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // newrightrs = lgr => resultSelector(default(TLeft), lgr.right)
        var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(resultSelector.Apply(argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector)  where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Cast<ParameterExpression>().Zip(args, (p, a) => (p, a))) {
            b = b.Replace(pa.p, pa.a);
        }

        return b.PropagateNull();
    }

    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
    public class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor {
        public readonly Expression from;
        public readonly Expression to;

        public ReplaceVisitor(Expression _from, Expression _to) {
            from = _from;
            to = _to;
        }

        public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
    }

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);
    public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
        public override Expression Visit(Expression node) {
            if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
                return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
            else
                return base.Visit(node);
        }
    }

    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }
}

NetMage, beeindruckende Codierung! Wenn ich es mit einem einfachen Beispiel ausführe und wenn [NullVisitor.Visit (..) in [base.Visit (Node)] aufgerufen wird, wird eine [System.ArgumentException: Argumenttypen stimmen nicht überein] ausgelöst. Was stimmt, da ich einen [Guid] TKey verwende und der Nullbesucher irgendwann einen [Guid?] - Typ erwartet. Vielleicht fehlt mir etwas. Ich habe ein kurzes Beispiel für EF 6.4.4 codiert. Bitte lassen Sie mich wissen, wie ich diesen Code mit Ihnen teilen kann. Vielen Dank!
Troncho

@Troncho Normalerweise verwende ich LINQPad zum Testen, daher ist EF 6 nicht einfach durchzuführen. base.Visit(node)sollte keine Ausnahme auslösen, da dies nur den Baum hinunter rekursiert. Ich kann auf so ziemlich jeden Code-Sharing-Dienst zugreifen, aber keine Testdatenbank einrichten. Das Ausführen mit meinem LINQ to SQL-Test scheint jedoch gut zu funktionieren.
NetMage

@Troncho Ist es möglich, dass Sie zwischen einem GuidSchlüssel und einem Guid?Fremdschlüssel verbinden?
NetMage

Ich benutze LinqPad auch zum Testen. Meine Abfrage löste die ArgumentException aus, sodass ich mich entschied, sie auf VS2019 unter [.Net Framework 4.7.1] und dem neuesten EF 6 zu debuggen. Dort musste ich das eigentliche Problem verfolgen. Um Ihren Code zu testen, generiere ich zwei separate Datensätze, die aus derselben [Personen] -Tabelle stammen. Ich filtere beide Sätze so, dass einige Datensätze für jeden Satz eindeutig sind und einige auf beiden Sätzen vorhanden sind. [PersonId] ist ein [Primärschlüssel] Guid (c #) / Uniqueidentifier (SqlServer) und keiner der beiden Sets generiert einen Nullwert [PersonId].
Freigegebener

1

Führt eine speicherinterne Streaming-Aufzählung über beide Eingänge durch und ruft den Selektor für jede Zeile auf. Wenn bei der aktuellen Iteration keine Korrelation besteht, ist eines der Auswahlargumente null .

Beispiel:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Benötigt einen IComparer für den Korrelationstyp, verwendet den Comparer.Default, falls nicht angegeben.

  • Erfordert, dass 'OrderBy' auf die Eingabeaufzählungen angewendet wird

    /// <summary>
    /// Performs a full outer join on two <see cref="IEnumerable{T}" />.
    /// </summary>
    /// <typeparam name="TLeft"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <typeparam name="TRight"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <param name="leftKeySelector"></param>
    /// <param name="rightKeySelector"></param>
    /// <param name="selector">Expression defining result type</param>
    /// <param name="keyComparer">A comparer if there is no default for the type</param>
    /// <returns></returns>
    [System.Diagnostics.DebuggerStepThrough]
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TValue, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TValue> leftKeySelector,
        Func<TRight, TValue> rightKeySelector,
        Func<TLeft, TRight, TResult> selector,
        IComparer<TValue> keyComparer = null)
        where TLeft: class
        where TRight: class
        where TValue : IComparable
    {
    
        keyComparer = keyComparer ?? Comparer<TValue>.Default;
    
        using (var enumLeft = left.OrderBy(leftKeySelector).GetEnumerator())
        using (var enumRight = right.OrderBy(rightKeySelector).GetEnumerator())
        {
    
            var hasLeft = enumLeft.MoveNext();
            var hasRight = enumRight.MoveNext();
            while (hasLeft || hasRight)
            {
    
                var currentLeft = enumLeft.Current;
                var valueLeft = hasLeft ? leftKeySelector(currentLeft) : default(TValue);
    
                var currentRight = enumRight.Current;
                var valueRight = hasRight ? rightKeySelector(currentRight) : default(TValue);
    
                int compare =
                    !hasLeft ? 1
                    : !hasRight ? -1
                    : keyComparer.Compare(valueLeft, valueRight);
    
                switch (compare)
                {
                    case 0:
                        // The selector matches. An inner join is achieved
                        yield return selector(currentLeft, currentRight);
                        hasLeft = enumLeft.MoveNext();
                        hasRight = enumRight.MoveNext();
                        break;
                    case -1:
                        yield return selector(currentLeft, default(TRight));
                        hasLeft = enumLeft.MoveNext();
                        break;
                    case 1:
                        yield return selector(default(TLeft), currentRight);
                        hasRight = enumRight.MoveNext();
                        break;
                }
            }
    
        }
    
    }

1
Das ist eine heldenhafte Anstrengung, um Dinge "streamen" zu lassen. Leider geht der gesamte Gewinn beim ersten Schritt verloren, bei dem Sie OrderBybeide Schlüsselprojektionen ausführen . OrderBypuffert die gesamte Sequenz aus den offensichtlichen Gründen .
sehe

@sehe Du bist definitiv richtig für Linq to Objects. Wenn die IEnumerable <T> IQueryable <T> sind, sollte die Quelle sortiert werden - es ist jedoch keine Zeit zum Testen. Wenn ich mich irre, sollte das einfache Ersetzen der Eingabe IEnumerable <T> durch IQueryable <T> in der Quelle / Datenbank sortiert werden.
James Caradoc-Davies

1

Meine saubere Lösung für die Situation, dass der Schlüssel in beiden Aufzählungen eindeutig ist:

 private static IEnumerable<TResult> FullOuterJoin<Ta, Tb, TKey, TResult>(
            IEnumerable<Ta> a, IEnumerable<Tb> b,
            Func<Ta, TKey> key_a, Func<Tb, TKey> key_b,
            Func<Ta, Tb, TResult> selector)
        {
            var alookup = a.ToLookup(key_a);
            var blookup = b.ToLookup(key_b);
            var keys = new HashSet<TKey>(alookup.Select(p => p.Key));
            keys.UnionWith(blookup.Select(p => p.Key));
            return keys.Select(key => selector(alookup[key].FirstOrDefault(), blookup[key].FirstOrDefault()));
        }

so

    var ax = new[] {
        new { id = 1, first_name = "ali" },
        new { id = 2, first_name = "mohammad" } };
    var bx = new[] {
        new { id = 1, last_name = "rezaei" },
        new { id = 3, last_name = "kazemi" } };

    var list = FullOuterJoin(ax, bx, a => a.id, b => b.id, (a, b) => "f: " + a?.first_name + " l: " + b?.last_name).ToArray();

Ausgänge:

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi

0

Vollständiger äußerer Join für zwei oder mehr Tabellen: Extrahieren Sie zuerst die Spalte, für die Sie einen Join erstellen möchten.

var DatesA = from A in db.T1 select A.Date; 
var DatesB = from B in db.T2 select B.Date; 
var DatesC = from C in db.T3 select C.Date;            

var Dates = DatesA.Union(DatesB).Union(DatesC); 

Verwenden Sie dann die linke äußere Verknüpfung zwischen der extrahierten Spalte und den Haupttabellen.

var Full_Outer_Join =

(from A in Dates
join B in db.T1
on A equals B.Date into AB 

from ab in AB.DefaultIfEmpty()
join C in db.T2
on A equals C.Date into ABC 

from abc in ABC.DefaultIfEmpty()
join D in db.T3
on A equals D.Date into ABCD

from abcd in ABCD.DefaultIfEmpty() 
select new { A, ab, abc, abcd })
.AsEnumerable();

0

Ich habe diese Erweiterungsklasse vor vielleicht 6 Jahren für eine App geschrieben und verwende sie seitdem in vielen Lösungen ohne Probleme. Ich hoffe es hilft.

Bearbeiten: Ich habe festgestellt, dass einige möglicherweise nicht wissen, wie eine Erweiterungsklasse verwendet wird.

Um diese Erweiterungsklasse zu verwenden, verweisen Sie einfach auf ihren Namespace in Ihrer Klasse, indem Sie die folgende Zeile mit joinext hinzufügen.

^ Dies sollte es Ihnen ermöglichen, die Intelligenz der Erweiterungsfunktionen in jeder IEnumerable-Objektsammlung zu sehen, die Sie gerade verwenden.

Hoffe das hilft. Lassen Sie mich wissen, wenn es immer noch nicht klar ist, und ich werde hoffentlich ein Beispielbeispiel für die Verwendung schreiben.

Hier ist die Klasse:

namespace joinext
{    
public static class JoinExtensions
    {
        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)
            where TInner : class
            where TOuter : class
        {
            var innerLookup = inner.ToLookup(innerKeySelector);
            var outerLookup = outer.ToLookup(outerKeySelector);

            var innerJoinItems = inner
                .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem)))
                .Select(innerItem => resultSelector(null, innerItem));

            return outer
                .SelectMany(outerItem =>
                {
                    var innerItems = innerLookup[outerKeySelector(outerItem)];

                    return innerItems.Any() ? innerItems : new TInner[] { null };
                }, resultSelector)
                .Concat(innerJoinItems);
        }


        public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return outer.GroupJoin(
                inner,
                outerKeySelector,
                innerKeySelector,
                (o, i) =>
                    new { o = o, i = i.DefaultIfEmpty() })
                    .SelectMany(m => m.i.Select(inn =>
                        resultSelector(m.o, inn)
                        ));

        }



        public static IEnumerable<TResult> RightJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return inner.GroupJoin(
                outer,
                innerKeySelector,
                outerKeySelector,
                (i, o) =>
                    new { i = i, o = o.DefaultIfEmpty() })
                    .SelectMany(m => m.o.Select(outt =>
                        resultSelector(outt, m.i)
                        ));

        }

    }
}

1
Leider scheint die Funktion in SelectManynicht in einen LINQ2SQL-würdigen Ausdrucksbaum konvertiert werden zu können.
ODER Mapper

edc65. Ich weiß, es könnte eine dumme Frage sein, wenn Sie das schon getan haben. Aber nur für den Fall (wie ich bemerkt habe, dass einige es nicht wissen), müssen Sie nur auf den Namespace joinext verweisen.
H7O

ODER Mapper, lassen Sie mich wissen, mit welcher Art von Sammlung Sie möchten, dass sie funktioniert. Es sollte gut mit jeder IEnumerable-Sammlung
funktionieren

0

Ich denke, dass die LINQ-Join-Klausel nicht die richtige Lösung für dieses Problem ist, da der Zweck der Join-Klausel nicht darin besteht, Daten so zu akkumulieren, wie es für diese Task-Lösung erforderlich ist. Der Code zum Zusammenführen erstellter separater Sammlungen wird zu kompliziert. Vielleicht ist er für Lernzwecke in Ordnung, aber nicht für echte Anwendungen. Eine Möglichkeit, dieses Problem zu lösen, finden Sie im folgenden Code:

class Program
{
    static void Main(string[] args)
    {
        List<FirstName> firstNames = new List<FirstName>();
        firstNames.Add(new FirstName { ID = 1, Name = "John" });
        firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

        List<LastName> lastNames = new List<LastName>();
        lastNames.Add(new LastName { ID = 1, Name = "Doe" });
        lastNames.Add(new LastName { ID = 3, Name = "Smith" });

        HashSet<int> ids = new HashSet<int>();
        foreach (var name in firstNames)
        {
            ids.Add(name.ID);
        }
        foreach (var name in lastNames)
        {
            ids.Add(name.ID);
        }
        List<FullName> fullNames = new List<FullName>();
        foreach (int id in ids)
        {
            FullName fullName = new FullName();
            fullName.ID = id;
            FirstName firstName = firstNames.Find(f => f.ID == id);
            fullName.FirstName = firstName != null ? firstName.Name : string.Empty;
            LastName lastName = lastNames.Find(l => l.ID == id);
            fullName.LastName = lastName != null ? lastName.Name : string.Empty;
            fullNames.Add(fullName);
        }
    }
}
public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}
class FullName
{
    public int ID;

    public string FirstName;

    public string LastName;
}

Wenn echte Sammlungen für die HashSet-Bildung groß sind, können stattdessen für jede Schleife der folgende Code verwendet werden:

List<int> firstIds = firstNames.Select(f => f.ID).ToList();
List<int> LastIds = lastNames.Select(l => l.ID).ToList();
HashSet<int> ids = new HashSet<int>(firstIds.Union(LastIds));//Only unique IDs will be included in HashSet

0

Vielen Dank an alle für die interessanten Beiträge!

Ich habe den Code geändert, weil ich ihn in meinem Fall brauchte

  • ein personalisiertes Join-Prädikat
  • ein personalisierter Gewerkschaftsvergleicher

Für diejenigen, die interessiert sind, ist dies mein geänderter Code (in VB, sorry)

    Module MyExtensions
        <Extension()>
        Friend Function FullOuterJoin(Of TA, TB, TResult)(ByVal a As IEnumerable(Of TA), ByVal b As IEnumerable(Of TB), ByVal joinPredicate As Func(Of TA, TB, Boolean), ByVal projection As Func(Of TA, TB, TResult), ByVal comparer As IEqualityComparer(Of TResult)) As IEnumerable(Of TResult)
            Dim joinL =
                From xa In a
                From xb In b.Where(Function(x) joinPredicate(xa, x)).DefaultIfEmpty()
                Select projection(xa, xb)
            Dim joinR =
                From xb In b
                From xa In a.Where(Function(x) joinPredicate(x, xb)).DefaultIfEmpty()
                Select projection(xa, xb)
            Return joinL.Union(joinR, comparer)
        End Function
    End Module

    Dim fullOuterJoin = lefts.FullOuterJoin(
        rights,
        Function(left, right) left.Code = right.Code And (left.Amount [...] Or left.Description.Contains [...]),
        Function(left, right) New CompareResult(left, right),
        New MyEqualityComparer
    )

    Public Class MyEqualityComparer
        Implements IEqualityComparer(Of CompareResult)

        Private Function GetMsg(obj As CompareResult) As String
            Dim msg As String = ""
            msg &= obj.Code & "_"
            [...]
            Return msg
        End Function

        Public Overloads Function Equals(x As CompareResult, y As CompareResult) As Boolean Implements IEqualityComparer(Of CompareResult).Equals
            Return Me.GetMsg(x) = Me.GetMsg(y)
        End Function

        Public Overloads Function GetHashCode(obj As CompareResult) As Integer Implements IEqualityComparer(Of CompareResult).GetHashCode
            Return Me.GetMsg(obj).GetHashCode
        End Function
    End Class

0

Noch eine vollständige äußere Verbindung

Da ich mit der Einfachheit und Lesbarkeit der anderen Sätze nicht so zufrieden war, kam ich zu folgendem Ergebnis:

Es hat nicht den Anspruch, schnell zu sein (ungefähr 800 ms, um 1000 * 1000 auf einer 2020m-CPU zu verbinden: 2,4 GHz / 2 Kerne). Für mich ist es nur eine kompakte und lässige vollständige äußere Verbindung.

Es funktioniert genauso wie ein SQL FULL OUTER JOIN (doppelte Speicherung)

Prost ;-)

using System;
using System.Collections.Generic;
using System.Linq;
namespace NS
{
public static class DataReunion
{
    public static List<Tuple<T1, T2>> FullJoin<T1, T2, TKey>(List<T1> List1, Func<T1, TKey> KeyFunc1, List<T2> List2, Func<T2, TKey> KeyFunc2)
    {
        List<Tuple<T1, T2>> result = new List<Tuple<T1, T2>>();

        Tuple<TKey, T1>[] identifiedList1 = List1.Select(_ => Tuple.Create(KeyFunc1(_), _)).OrderBy(_ => _.Item1).ToArray();
        Tuple<TKey, T2>[] identifiedList2 = List2.Select(_ => Tuple.Create(KeyFunc2(_), _)).OrderBy(_ => _.Item1).ToArray();

        identifiedList1.Where(_ => !identifiedList2.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(_.Item2, default(T2)));
        });

        result.AddRange(
            identifiedList1.Join(identifiedList2, left => left.Item1, right => right.Item1, (left, right) => Tuple.Create<T1, T2>(left.Item2, right.Item2)).ToList()
        );

        identifiedList2.Where(_ => !identifiedList1.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(default(T1), _.Item2));
        });

        return result;
    }
}
}

Die Idee ist zu

  1. Erstellen Sie IDs basierend auf den bereitgestellten Schlüsselfunktions-Buildern
  2. Nur übrig gebliebene Elemente verarbeiten
  3. Verarbeiten Sie die innere Verknüpfung
  4. Nur richtig verarbeitete Artikel

Hier ist ein prägnanter Test, der dazu gehört:

Platzieren Sie am Ende einen Haltepunkt, um manuell zu überprüfen, ob er sich wie erwartet verhält

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NS;

namespace Tests
{
[TestClass]
public class DataReunionTest
{
    [TestMethod]
    public void Test()
    {
        List<Tuple<Int32, Int32, String>> A = new List<Tuple<Int32, Int32, String>>();
        List<Tuple<Int32, Int32, String>> B = new List<Tuple<Int32, Int32, String>>();

        Random rnd = new Random();

        /* Comment the testing block you do not want to run
        /* Solution to test a wide range of keys*/

        for (int i = 0; i < 500; i += 1)
        {
            A.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "A"));
            B.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "B"));
        }

        /* Solution for essential testing*/

        A.Add(Tuple.Create(1, 2, "B11"));
        A.Add(Tuple.Create(1, 2, "B12"));
        A.Add(Tuple.Create(1, 3, "C11"));
        A.Add(Tuple.Create(1, 3, "C12"));
        A.Add(Tuple.Create(1, 3, "C13"));
        A.Add(Tuple.Create(1, 4, "D1"));

        B.Add(Tuple.Create(1, 1, "A21"));
        B.Add(Tuple.Create(1, 1, "A22"));
        B.Add(Tuple.Create(1, 1, "A23"));
        B.Add(Tuple.Create(1, 2, "B21"));
        B.Add(Tuple.Create(1, 2, "B22"));
        B.Add(Tuple.Create(1, 2, "B23"));
        B.Add(Tuple.Create(1, 3, "C2"));
        B.Add(Tuple.Create(1, 5, "E2"));

        Func<Tuple<Int32, Int32, String>, Tuple<Int32, Int32>> key = (_) => Tuple.Create(_.Item1, _.Item2);

        var watch = System.Diagnostics.Stopwatch.StartNew();
        var res = DataReunion.FullJoin(A, key, B, key);
        watch.Stop();
        var elapsedMs = watch.ElapsedMilliseconds;
        String aser = JToken.FromObject(res).ToString(Formatting.Indented);
        Console.Write(elapsedMs);
    }
}

}}


-4

Ich hasse diese linq-Ausdrücke wirklich, deshalb gibt es SQL:

select isnull(fn.id, ln.id) as id, fn.firstname, ln.lastname
   from firstnames fn
   full join lastnames ln on ln.id=fn.id

Erstellen Sie diese als SQL-Ansicht in der Datenbank und importieren Sie sie als Entität.

Natürlich wird es auch eine (eindeutige) Vereinigung von linken und rechten Verbindungen schaffen, aber es ist dumm.


11
Warum nicht einfach so viele Abstraktionen wie möglich ablegen und dies im Maschinencode tun? (Hinweis: weil die Abstraktionen höherer Ordnung dem Programmierer das Leben erleichtern). Dies beantwortet die Frage nicht und sieht für mich eher wie ein Schimpfen gegen LINQ aus.
Spender

8
Wer hat gesagt, dass die Daten aus einer Datenbank stammen?
user247702

1
Natürlich ist es Datenbank, es gibt Wörter "Outer Join" in Frage :) google.cz/search?q=outer+join
Milan Švec

1
Ich verstehe, dass dies eine "altmodische" Lösung ist, aber vergleichen Sie vor dem Downvoting ihre Komplexität mit anderen Lösungen :) Mit Ausnahme der akzeptierten ist es natürlich die richtige.
Milan Švec

Natürlich kann es eine Datenbank sein oder nicht. Ich
suche
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.