LINQ - Full Outer Join


202

Mam listę osób i ich imię oraz listę osób i ich nazwiska. Niektóre osoby nie mają imienia, a niektóre nie mają nazwiska; Chciałbym wykonać pełne zewnętrzne połączenie na dwóch listach.

Więc następujące listy:

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

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

Powinien produkować:

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

Jestem nowy w LINQ (więc wybacz mi, gdy jestem kulawy) i znalazłem sporo rozwiązań dla „Zewnętrznych połączeń LINQ”, które wyglądają dość podobnie, ale tak naprawdę wydają się być zewnętrznymi złączeniami.

Moje dotychczasowe próby idą mniej więcej tak:

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

Ale to zwraca:

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

Co ja robię źle?


2
Czy potrzebujesz tego, aby działał tylko dla list w pamięci, czy dla Linq2Sql?
JamesFaix,

Odpowiedzi:


122

Nie wiem, czy dotyczy to wszystkich przypadków, logicznie wydaje się to poprawne. Chodzi o to, aby wziąć lewe łączenie zewnętrzne i prawe połączenie zewnętrzne, a następnie wziąć połączenie wyników.

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

Działa to tak, jak napisano, ponieważ znajduje się w LINQ to Objects. Jeśli LINQ to SQL lub inny, procesor zapytań może nie obsługiwać bezpiecznej nawigacji lub innych operacji. Musisz użyć operatora warunkowego, aby warunkowo uzyskać wartości.

to znaczy,

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
Unia wyeliminuje duplikaty. Jeśli nie oczekujesz duplikatów lub możesz napisać drugie zapytanie, aby wykluczyć wszystko, co zostało uwzględnione w pierwszym, użyj Concat. To jest różnica SQL między UNION a UNION ALL
cadrell0

3
@ cadre110 duplikaty wystąpią, jeśli dana osoba ma imię i nazwisko, więc związek jest prawidłowym wyborem.
saus

1
@saus, ale istnieje kolumna identyfikatora, więc nawet jeśli istnieje zduplikowane imię i nazwisko, identyfikator powinien być inny
cadrell0

1
Twoje rozwiązanie działa na prymitywne typy, ale wydaje się, że nie działa na obiekty. W moim przypadku FirstName to obiekt domeny, a LastName to inny obiekt domeny. Kiedy łączę dwa wyniki, LINQ zgłosiło NotSupportedException (typy w Union lub Concat są zbudowane niekompatybilnie). Czy doświadczyłeś podobnych problemów?
Candy Chiu

1
@CandyChiu: Właściwie nigdy nie spotkałem się z taką sprawą. Wydaje mi się, że jest to ograniczenie dla twojego dostawcy zapytań. Prawdopodobnie będziesz chciał użyć LINQ do Objects w tym przypadku, dzwoniąc AsEnumerable()przed wykonaniem unii / konkatenacji. Spróbuj i przekonaj się, jak to idzie. Jeśli to nie jest droga, którą chcesz się udać, nie jestem pewien, czy mogę ci w czymś pomóc.
Jeff Mercado

196

Aktualizacja 1: zapewnienie prawdziwie uogólnionej metody rozszerzenia FullOuterJoin
Aktualizacja 2: opcjonalnie akceptacja niestandardowego IEqualityComparertypu klucza
Aktualizacja 3 : ta implementacja stała się niedawno częściąMoreLinq - Dzięki, chłopaki!

Edytuj dodane FullOuterGroupJoin( ideone ). Użyłem ponownieGetOuter<> implementację, dzięki czemu ta część jest mniej wydajna, niż mogłaby być, ale obecnie dążę do kodu „wysokiego poziomu”, a nie zoptymalizowanego pod kątem najnowszych technologii.

Zobacz na żywo na 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);
}

Drukuje dane wyjściowe:

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

Możesz także podać wartości domyślne: 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)" }
        )

Druk:

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

Wyjaśnienie użytych terminów:

Dołączanie jest terminem zapożyczonym z projektu relacyjnej bazy danych:

  • Dołączyć powtórzy elementy ze atyle razy, ile istnieją elementy b z odpowiadającym kluczem (tzn nic jeśli bbyły puste). Lingo bazy danych nazywa toinner (equi)join .
  • Sprzężenie zewnętrzne obejmuje elementy, adla których nie istnieje odpowiedni elementb . (tzn .: nawet wyniki, jeśli bbyły puste). Jest to zwykle określane jakoleft join .
  • Pełnego sprzężenia zewnętrznego zawiera rekordy z a , jak równieżb , jeżeli nie ma odpowiedni element istnieje w drugiej. (tzn. nawet wyniki, jeśli abyły puste)

Coś, czego zwykle nie widać w RDBMS, to dołączenie do grupy [1] :

  • Grupa przyłączenia , czy takie same, jak opisano powyżej, lecz zamiast tego z powtarzających się elementów ao wielokrotność odpowiada b, to grupy Rekordy z odpowiednich klawiszy. Jest to często wygodniejsze, gdy chcesz wyliczyć rekordy „połączone” na podstawie wspólnego klucza.

Zobacz także GroupJoin, który zawiera również ogólne wyjaśnienia.


[1] (Wierzę, że Oracle i MSSQL mają do tego zastrzeżone rozszerzenia)

Pełny kod

Uogólniona klasa rozszerzenia „drop-in” do tego celu

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

Edytowano, aby pokazać użycie FullOuterJoinpodanej metody rozszerzenia
patrz

Edytowano: Dodano metodę rozszerzenia FullOuterGroupJoin
patrz

4
Zamiast używać słownika, możesz użyć odnośnika , który zawiera funkcje wyrażone w metodach rozszerzenia pomocnika. Na przykład możesz pisać a.GroupBy(selectKeyA).ToDictionary();jako a.ToLookup(selectKeyA)i adict.OuterGet(key)jako alookup[key]. Pierwsze odbioru kluczy jest trochę trudniejsze, ale: alookup.Select(x => x.Keys).
Ryzykowny Martin

1
@RiskyMartin Thanks! To sprawia, że ​​całość jest bardziej elegancka. Zaktualizowałem odpowiedź i ideone. (Przypuszczam, że wydajność powinna zostać zwiększona, ponieważ tworzy się mniej obiektów).
patrz

1
@Revious to działa tylko wtedy, gdy wiesz, że klucze są unikalne. I to nie jest typowy przypadek dla / grouping /. Poza tym, tak, z całą pewnością. Jeśli wiesz, że hash nie będzie przeciągał perf (kontenery oparte na węzłach mają w zasadzie więcej kosztów, a hashowanie nie jest darmowe, a wydajność zależy od funkcji skrótu / rozproszenia wiadra), z pewnością będzie bardziej wydajna algorytmicznie. Tak więc, dla małych ładunków spodziewałbym się, że może nie być szybszy
patrz

27

Myślę, że są problemy z większością z nich, w tym z zaakceptowaną odpowiedzią, ponieważ nie działają one dobrze z Linq nad IQueryable albo z powodu zbyt dużej liczby podróży w obie strony na serwerze i zbyt dużej liczby zwrotów danych, albo zbyt dużej liczby klientów.

Dla IEnumerable nie podoba mi się odpowiedź Sehe lub podobna, ponieważ ma nadmierne wykorzystanie pamięci (prosty test 10000000 dwóch list uruchomił Linqpad z pamięci na moim komputerze o pojemności 32 GB).

Ponadto większość innych nie wdraża właściwie pełnego pełnego połączenia zewnętrznego, ponieważ używają Unii z prawym złączem zamiast Concat z prawym łączeniem anty-częściowym, co nie tylko eliminuje duplikaty wewnętrznych rzędów złączeń z wyniku, ale także wszelkie prawidłowe duplikaty istniejące pierwotnie w danych po lewej lub po prawej stronie.

Oto moje rozszerzenia, które obsługują wszystkie te problemy, generują SQL, a także implementują bezpośrednie dołączanie do LINQ do SQL, uruchamiają się na serwerze i są szybsze i mają mniej pamięci niż inne na Enumerables:

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

Różnica między prawym łączeniem częściowym jest głównie dyskusyjna z Linq do Objects lub w źródle, ale robi różnicę po stronie serwera (SQL) w ostatecznej odpowiedzi, usuwając niepotrzebne JOIN.

Ręczne kodowanie Expressionobsługi połączeniaExpression<Func<>>LinqKit może usprawnić w lambda, ale byłoby miło, gdyby język / kompilator dodał do tego jakąś pomoc. Funkcje FullOuterJoinDistincti RightOuterJoinsą włączone dla kompletności, ale nie wdrożyłem FullOuterGroupJoinjeszcze.

napisałem inną wersję pełnego sprzężenia zewnętrznego dlaIEnumerable dla przypadków, w których klucz można zamówić, co jest około 50% szybsze niż połączenie lewego sprzężenia zewnętrznego z prawym łączeniem anty-pół, przynajmniej w małych kolekcjach. Przechodzi przez każdą kolekcję po sortowaniu tylko raz.

Dodałem także inną odpowiedź dla wersji, która działa z EF, zastępując Invokeniestandardowe rozszerzenie.


O co chodzi TP unusedP, TC unusedC? Czy są dosłownie nieużywane?
Rudey,

Tak, są one po prostu obecny uchwycić typów w TP, TC, TResultaby stworzyć właściwy Expression<Func<>>. Mam mogę je zastąpić _, __, ___zamiast, ale to nie wydaje się być jaśniejsze, aż C # ma odpowiednią wieloznacznego zamiast parametru do używania.
NetMage

1
@MarcL. Nie jestem pewien co do „męczącego” - ale zgadzam się, że ta odpowiedź jest bardzo przydatna w tym kontekście. Imponujące rzeczy (chociaż dla mnie potwierdza to wady Linq-SQL)
patrz

3
Dostaję The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. Czy są jakieś ograniczenia związane z tym kodem? Chcę wykonać PEŁNE DOŁĄCZENIE do IQueryables
ucznia

1
Dodałem nową odpowiedź, która zastępuje Invokeniestandardową, ExpressionVisitoraby wstawić, Invokewięc powinna działać z EF. Możesz spróbować
NetMage

7

Oto metoda rozszerzenia, która to robi:

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), co oznacza pełne połączenie zewnętrzne = lewe połączenie zewnętrzne lewe, wszystkie prawe połączenie zewnętrzne! Doceniam prostotę tego podejścia.
TamusJRoyce

1
@TamusJRoyce z wyjątkiem Unionusuwa duplikaty, więc jeśli w oryginalnych danych są zduplikowane wiersze, nie będzie ich rezultatem.
NetMage 31.01.19

Świetny punkt! dodaj unikalny identyfikator, jeśli chcesz zapobiec usuwaniu duplikatów. Tak. Związek jest trochę marnotrawiony, chyba że można wskazać, że istnieje unikalny identyfikator, a związek przełącza się na związek wszystkie (za pomocą wewnętrznej heurystyki / optymalizacji). Ale to zadziała.
TamusJRoyce


7

Zgaduję, że podejście @ sehe jest silniejsze, ale dopóki nie zrozumiem go lepiej, odkrywam, że przeskakuję nad rozszerzeniem @ MichaelSander. Zmodyfikowałem go, aby dopasować składnię i typ zwracanej wbudowanej metody Enumerable.Join () opisanej tutaj . Dołączyłem „wyraźny” przyrostek w odniesieniu do komentarza @ cadrell0 pod rozwiązaniem @ JeffMercado.

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

}

W tym przykładzie użyłbyś tego w następujący sposób:

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

W przyszłości, gdy będę się więcej uczyć, mam wrażenie, że migruję do logiki @ sehe, biorąc pod uwagę jej popularność. Ale nawet wtedy będę musiał zachować ostrożność, ponieważ uważam, że ważne jest, aby mieć co najmniej jedno przeciążenie, które pasuje do składni istniejącej metody „.Join ()”, jeśli jest to możliwe, z dwóch powodów:

  1. Spójność metod pomaga zaoszczędzić czas, uniknąć błędów i uniknąć niezamierzonego zachowania.
  2. Jeśli kiedykolwiek w przyszłości pojawi się gotowa metoda „.FullJoin ()”, wyobrażam sobie, że spróbuje zachować składnię obecnie istniejącej metody „.Join ()”, jeśli to możliwe. Jeśli tak, to jeśli chcesz przeprowadzić migrację, możesz po prostu zmienić nazwę swoich funkcji bez zmiany parametrów lub martwienia się o to, że różne typy zwrotów psują kod.

Nadal jestem nowy z ogólnymi, rozszerzeniami, instrukcjami Func i innymi funkcjami, więc opinie są z pewnością mile widziane.

EDYTOWAĆ: Nie zajęło mi długo uświadomienie sobie, że wystąpił problem z moim kodem. Robiłem .Dump () w LINQPad i szukałem typu zwracanego. To było po prostu niezliczone, więc próbowałem to dopasować. Ale kiedy faktycznie zrobiłem .Where () lub .Select () na moim rozszerzeniu, dostałem błąd: „System Collection.IEnumerable” nie zawiera definicji „Select” i… ”. W końcu udało mi się dopasować składnię wejściową .Join (), ale nie zachowanie powrotu.

EDYCJA: Dodano „TResult” do typu zwracanego dla funkcji. Pominął to podczas czytania artykułu Microsoft i oczywiście ma to sens. Dzięki tej poprawce wydaje się, że zachowanie powrotu jest zgodne z moimi celami.


+2 za tę odpowiedź, podobnie jak Michael Sanders. Przypadkowo kliknąłem ten przycisk i głosowanie jest zablokowane. Dodaj dwa.
TamusJRoyce

@TamusJRoyce, właśnie poszedłem trochę edytować formaty kodu. Wierzę, że po dokonaniu edycji masz możliwość ponownego przetworzenia swojego głosu. Spróbuj, jeśli chcesz.
pwilcox

Dziękuję bardzo!
Roshna Omer,

6

Jak już odkryłeś, Linq nie ma konstrukcji „łączenia zewnętrznego”. Najbliższe, jakie możesz uzyskać, to lewe połączenie zewnętrzne za pomocą podanego zapytania. Do tego możesz dodać dowolne elementy listy nazwisk, które nie są reprezentowane w złączeniu:

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

Podoba mi się odpowiedź sehe, ale nie wykorzystuje ona odroczonego wykonania (sekwencje wejściowe są chętnie wyliczane przez wywołania ToLookup). Po przejrzeniu źródeł .NET dla LINQ-to-objects , wymyśliłem:

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

Ta implementacja ma następujące ważne właściwości:

  • Odroczone wykonanie, sekwencje wejściowe nie zostaną wyliczone przed wyliczeniem sekwencji wyjściowej.
  • Wymienia sekwencje wejściowe tylko raz.
  • Zachowuje kolejność sekwencji wejściowych w tym sensie, że da krotki w kolejności lewej sekwencji, a następnie prawej (dla klawiszy nieobecnych w lewej sekwencji).

Te właściwości są ważne, ponieważ są tym, czego oczekuje ktoś nowy w FullOuterJoin, ale doświadczony w LINQ.


Nie zachowuje kolejności sekwencji wejściowych: Wyszukiwanie nie gwarantuje tego, więc te kazania będą wyliczać w pewnej kolejności po lewej stronie, a następnie w pewnym porządku po prawej stronie nieobecnej po lewej stronie. Ale relacyjny porządek elementów nie jest zachowany.
Ivan Danilov

@IvanDanilov Masz rację, że tak naprawdę nie ma tego w umowie. Implementacja ToLookup wykorzystuje jednak wewnętrzną klasę odnośników w Enumerable.cs, która utrzymuje grupy na uporządkowanej liście uporządkowanej za pomocą wstawiania i używa tej listy do iteracji. Tak więc w obecnej wersji .NET kolejność jest gwarantowana, ale ponieważ MS niestety tego nie udokumentował, mogą to zmienić w późniejszych wersjach.
Søren Boisen

Wypróbowałem to na .NET 4.5.1 na Win 8.1 i nie zachowuje porządku.
Ivan Danilov

1
„... sekwencje wejściowe są chętnie wyliczane przez wywołania ToLookup”. Ale twoja implementacja robi dokładnie to samo. Wydajność nie daje tu wiele ze względu na wydatki na maszynę o skończonym stanie.
pkuderov

4
Wywołania wyszukiwania są wykonywane, gdy żądany jest pierwszy element wyniku, a nie podczas tworzenia iteratora. To właśnie oznacza odroczenie wykonania. Można jeszcze bardziej odroczyć wyliczanie jednego zestawu danych wejściowych, iterując bezpośrednio lewy Enumerable zamiast przekształcić go w odnośnik, co daje dodatkową korzyść polegającą na zachowaniu kolejności lewego zestawu.
Rolf

2

Postanowiłem dodać to jako osobną odpowiedź, ponieważ nie jestem pewien, czy jest wystarczająco przetestowany. Jest to ponowna implementacja FullOuterJoinmetody przy użyciu zasadniczo uproszczonej, dostosowanej wersji LINQKit Invoke/ Expandfor Expression, aby działała w Entity Framework. Nie ma wielu wyjaśnień, ponieważ jest prawie taka sama jak moja poprzednia odpowiedź.

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, imponujące kodowanie! Kiedy uruchamiam go z prostym przykładem i kiedy [NullVisitor.Visit (..) zostaje wywołane w [base.Visit (Node)], generuje [System.ArgumentException: Typy argumentów nie pasują]. Co jest prawdą, ponieważ używam klucza [Guid] i w pewnym momencie użytkownik o zerowej wartości oczekuje typu [Guid?]. Być może coś mi brakuje. Mam krótki przykład zakodowany dla EF 6.4.4. Daj mi znać, jak mogę udostępnić Ci ten kod. Dzięki!
Troncho

@Troncho Zwykle używam LINQPada do testowania, więc EF 6 nie jest łatwy do wykonania. base.Visit(node)nie powinien rzucać wyjątku, ponieważ po prostu powraca w dół drzewa. Mogę uzyskać dostęp do praktycznie każdej usługi udostępniania kodu, ale nie mogę skonfigurować testowej bazy danych. Jednak uruchomienie go z moim testem LINQ to SQL działa dobrze.
NetMage

@Troncho Czy możliwe jest łączenie Guidklucza z kluczem Guid?obcym?
NetMage

Używam LinqPad również do testowania. Moje zapytanie wyrzuciło ArgumentException, więc postanowiłem debugować go na VS2019 na [.Net Framework 4.7.1] i najnowszym EF 6. Tam muszę prześledzić prawdziwy problem. Aby przetestować kod, generuję 2 oddzielne zestawy danych pochodzące z tej samej tabeli [Osoby]. Filtruję oba zestawy, aby niektóre rekordy były unikalne dla każdego zestawu, a niektóre istniały w obu zestawach. [PersonId] to [Podstawowy klucz] Guid (c #) / Uniqueidentifier (SqlServer) i żaden zestaw nie generuje żadnej wartości [PersonId]. Wspólny kod: github.com/Troncho/EF_FullOuterJoin
Troncho

1

Wykonuje wyliczenie strumieniowania w pamięci na obu wejściach i wywołuje selektor dla każdego wiersza. Jeśli w bieżącej iteracji nie ma korelacji, jeden z argumentów selektora będzie pusty .

Przykład:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Wymaga IComparer dla typu korelacji, używa funkcji Comparer.Default, jeśli nie jest podana.

  • Wymaga zastosowania „OrderBy” do wejściowych elementów wyliczeniowych

    /// <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
To heroiczny wysiłek, aby sprawić, że rzeczy „będą strumieniować”. Niestety cały zysk zostaje utracony na pierwszym etapie, w którym wykonujesz OrderByoba kluczowe projekcje. OrderBybuforuje całą sekwencję, z oczywistych powodów .
patrz

@sehe Zdecydowanie masz rację dla Linq to Objects. Jeśli IEnumerable <T> są IQueryable <T>, źródło powinno się posortować - nie ma jednak czasu na testowanie. Jeśli się mylę co do tego, po prostu zastąpienie wejściowej IEnumerable <T> IQueryable <T> powinno posortować w źródle / bazie danych.
James Caradoc-Davies

1

Moje czyste rozwiązanie dla sytuacji, w której klucz jest unikalny w obu wyliczeniach:

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

więc

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

wyjścia:

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

0

Pełne sprzężenie zewnętrzne dla dwóch lub więcej tabel: Najpierw rozpakuj kolumnę, do której chcesz dołączyć.

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

Następnie użyj lewego połączenia zewnętrznego między wyodrębnioną kolumną a tabelami głównymi.

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

Napisałem tę klasę rozszerzeń dla aplikacji, być może 6 lat temu, i używam jej odtąd w wielu rozwiązaniach bez problemów. Mam nadzieję, że to pomoże.

edycja: Zauważyłem, że niektórzy mogą nie wiedzieć, jak korzystać z klasy rozszerzenia.

Aby użyć tej klasy rozszerzenia, po prostu odwołaj się do jej przestrzeni nazw w swojej klasie, dodając następujący wiersz za pomocą joinext;

^ to powinno pozwolić ci zobaczyć inteligencję funkcji rozszerzeń w dowolnej kolekcji obiektów IEnumerable, której używasz.

Mam nadzieję że to pomoże. Daj mi znać, jeśli nadal nie jest jasne, i mam nadzieję, że napiszę przykładowy przykład, jak go używać.

Oto klasa:

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
Niestety wydaje się, że funkcji w SelectManynie można przekonwertować na drzewo wyrażeń godne LINQ2SQL.
LUB Mapper

edc65. Wiem, że to głupie pytanie, jeśli już to zrobiłeś. Ale na wszelki wypadek (jak zauważyłem niektórzy nie wiem), wystarczy odwołać się do joinext przestrzeni nazw.
H7O,

LUB Mapper, daj mi znać z jakim rodzajem kolekcji, która ma działać. Powinien działać dobrze z każdą kolekcją IEnumerable
H7O

0

Myślę, że klauzula łączenia LINQ nie jest właściwym rozwiązaniem tego problemu, ponieważ celem klauzuli przyłączenia nie jest gromadzenie danych w sposób wymagany dla tego rozwiązania zadania. Kod do scalania utworzonych oddzielnych kolekcji staje się zbyt skomplikowany, być może jest odpowiedni do celów edukacyjnych, ale nie do prawdziwych aplikacji. Jednym ze sposobów rozwiązania tego problemu jest poniższy kod:

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

Jeśli rzeczywiste kolekcje są duże do tworzenia HashSet, zamiast tego można użyć pętli foreach:

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

Dziękujemy wszystkim za ciekawe posty!

Zmodyfikowałem kod, ponieważ w moim przypadku potrzebowałem

  • za spersonalizowane dołączyć orzeczenie
  • spersonalizowany podmiot porównujący odrębny związek

Dla zainteresowanych jest to mój zmodyfikowany kod (w VB, przepraszam)

    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

Kolejne pełne zewnętrzne połączenie

Ponieważ nie byłem zadowolony z prostoty i czytelności innych zdań, skończyłem z tym:

Nie ma pretensji, aby być szybkim (około 800 ms, aby dołączyć do 1000 * 1000 na procesorze 2020m: 2,4 GHz / 2 rdzenie). Dla mnie to tylko kompaktowe i swobodne pełne połączenie zewnętrzne.

Działa tak samo jak SQL FULL OUTER JOIN (ochrona duplikatów)

Twoje zdrowie ;-)

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

Chodzi o to, aby

  1. Kompiluj identyfikatory na podstawie dostarczonych konstruktorów funkcji kluczowych
  2. Przetwarzaj tylko pozostawione elementy
  3. Przetwórz połączenie wewnętrzne
  4. Przetwarzaj tylko odpowiednie elementy

Oto zwięzły test, który się z tym wiąże:

Umieść punkt przerwania na końcu, aby ręcznie sprawdzić, czy zachowuje się zgodnie z oczekiwaniami

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

Naprawdę nienawidzę tych wyrażeń linq, dlatego SQL istnieje:

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

Utwórz to jako widok SQL w bazie danych i zaimportuj jako encję.

Oczywiście (wyraźne) połączenie lewych i prawych złączeń też sprawi, że będzie, ale to jest głupie.


11
Dlaczego po prostu nie upuścić jak największej liczby abstrakcji i zrobić to w kodzie maszynowym? (Wskazówka: ponieważ abstrakcje wyższego rzędu ułatwiają programistom życie). To nie odpowiada na pytanie i wydaje mi się bardziej rantem przeciwko LINQ.
spędzić

8
Kto powiedział, że dane pochodzą z bazy danych?
user247702

1
Oczywiście, jest to baza danych, w pytaniu są słowa „zewnętrzny łączenie” :) google.cz/search?q=outer+join
Milan Švec

1
Rozumiem, że jest to rozwiązanie „staromodne”, ale przed oddaniem głosu porównaj jego złożoność z innymi rozwiązaniami :) Poza przyjętym, jest ono oczywiście poprawne.
Milan Švec

Oczywiście może to być baza danych lub nie. Szukam rozwiązania z zewnętrznym połączeniem między listami w pamięci
edc65
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.