Jak wykonać lewe połączenie zewnętrzne za pomocą metod rozszerzenia linq

272

Zakładając, że mam lewe połączenie zewnętrzne jako takie:

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

Jak wyraziłbym to samo zadanie za pomocą metod rozszerzenia? Na przykład

Foo.GroupJoin(Bar, f => f.Foo_Id, b => b.Foo_Id, (f,b) => ???)
    .Select(???)
LaserJesus
źródło

Odpowiedzi:

444

Aby połączyć (lewy zewnętrzny) stół Barze stołem Foowłączonym Foo.Foo_Id = Bar.Foo_Idw notacji lambda:

var qry = Foo.GroupJoin(
          Bar, 
          foo => foo.Foo_Id,
          bar => bar.Foo_Id,
          (x,y) => new { Foo = x, Bars = y })
       .SelectMany(
           x => x.Bars.DefaultIfEmpty(),
           (x,y) => new { Foo=x.Foo, Bar=y});
Marc Gravell
źródło
27
To nie jest tak szalone, jak się wydaje. Zasadniczo GroupJoinrobi lewe połączenie zewnętrzne, SelectManyczęść jest potrzebna tylko w zależności od tego, co chcesz wybrać.
George Mauer
6
Ten wzór jest świetny, ponieważ Entity Framework rozpoznaje to jako lewe
łączenie
3
@nam Cóż, potrzebujesz instrukcji where, x.Bar == null
Tod
2
@AbdulkarimKanaan tak - SelectMany spłaszcza dwie warstwy 1-wielu w 1 warstwę z wejściem na parę
Marc Gravell
1
@MarcGravell Zasugerowałem edycję, aby dodać trochę wyjaśnienia tego, co zrobiłeś we frustracji kodu.
B - rian
109

Ponieważ wydaje się, że jest to de facto pytanie SO dla lewych złączeń zewnętrznych przy użyciu składni metody (rozszerzenia), pomyślałem, że dodam alternatywę do obecnie wybranej odpowiedzi, która (przynajmniej z mojego doświadczenia) była częściej tym, czym jestem po

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

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

Aby wyświetlić różnicę za pomocą prostego zestawu danych (zakładając, że łączymy się na samych wartościach):

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

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

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

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

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

Opcja 2 jest zgodna z typową lewą definicją łączenia zewnętrznego, ale jak wspomniałem wcześniej, jest często niepotrzebnie złożona w zależności od zestawu danych.

Ocelot20
źródło
7
Myślę, że „bs.SingleOrDefault ()” nie będzie działać, jeśli masz kolejne następujące polecenia Join lub Include. W takich przypadkach potrzebujemy „bs.FirstOrDefault ()”.
Dherik
3
To prawda, że ​​Entity Framework i Linq to SQL wymagają tego, ponieważ nie mogą łatwo wykonać Singlesprawdzenia pośrodku łączenia. SingleOrDefaultjest jednak bardziej „poprawnym” sposobem wykazania tego IMO.
Ocelot20
1
Musisz pamiętać, aby zamówić połączoną tabelę, ponieważ .FirstOrDefault () otrzyma losowy wiersz z wielu wierszy, które mogą pasować do kryteriów łączenia, niezależnie od tego, co baza danych znajdzie najpierw.
Chris Moschini,
1
@ChrisMoschini: Order i FirstOrDefault są niepotrzebne, ponieważ przykład dotyczy dopasowania 0 lub 1, w którym chciałbyś zawieść na wielu rekordach (patrz komentarz powyżej kodu).
Ocelot20
2
Nie jest to „dodatkowy wymóg” nieokreślony w pytaniu, ale o tym myśli wiele osób, gdy mówią „lewe przyłączenie zewnętrzne”. Ponadto wymaganie FirstOrDefault, o którym wspomina Dherik, to zachowanie EF / L2SQL, a nie L2Objects (żaden z nich nie znajduje się w tagach). SingleOrDefault jest absolutnie poprawną metodą do wywołania w tym przypadku. Oczywiście chcesz zgłosić wyjątek, jeśli napotkasz więcej rekordów niż to możliwe dla zestawu danych, zamiast wybierać dowolny, co prowadzi do mylącego, nieokreślonego wyniku.
Ocelot20
52

Metoda łączenia grupowego nie jest konieczna do uzyskania połączenia dwóch zestawów danych.

Przyłączenie wewnętrzne:

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

Dla Left Join wystarczy dodać DefaultIfEmpty ()

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

EF i LINQ na SQL poprawnie przekształcają się w SQL. W przypadku LINQ to Objects lepiej dołączyć przy użyciu GroupJoin, ponieważ wewnętrznie korzysta z wyszukiwania . Ale jeśli pytasz DB, pomijanie GroupJoin jest AFAIK jako wydajne.

Personlay dla mnie w ten sposób jest bardziej czytelny w porównaniu do GroupJoin ().

Gediminas Zimkus
źródło
Udało mi się to lepiej niż .Join dla mnie, a ponadto mogłem wykonać mój warunkowy staw, który chciałem (prawo. FooId == left.FooId || right.FooId == 0)
Anders
linq2sql tłumaczy to podejście jako lewe złączenie. ta odpowiedź jest lepsza i prostsza. +1
Guido Mocha,
15

Możesz utworzyć metodę rozszerzenia, taką jak:

public static IEnumerable<TResult> LeftOuterJoin<TSource, TInner, TKey, TResult>(this IEnumerable<TSource> source, IEnumerable<TInner> other, Func<TSource, TKey> func, Func<TInner, TKey> innerkey, Func<TSource, TInner, TResult> res)
    {
        return from f in source
               join b in other on func.Invoke(f) equals innerkey.Invoke(b) into g
               from result in g.DefaultIfEmpty()
               select res.Invoke(f, result);
    }
hajirazin
źródło
Wygląda na to, że zadziałałoby (jak na moje wymagania). Czy możesz podać przykład? Jestem nowy w rozszerzeniach LINQ i trudno mi owinąć głowę wokół tej sytuacji Lewego Dołączenia Jestem w ...
Shiva
@ Skychan Być może muszę na to spojrzeć, to stara odpowiedź i wtedy działał. Z jakiego frameworku korzystasz? Mam na myśli wersję .NET?
hajirazin
2
Działa to w przypadku Linq to Objects, ale nie w przypadku zapytania do bazy danych, ponieważ musisz operować na IQuerable i zamiast tego używać wyrażeń Funcs
Bob Vale
4

Poprawiając odpowiedź Ocelot20, jeśli masz stolik, na którym chcesz się połączyć zewnętrznie, z którego chcesz tylko 0 lub 1 wierszy, ale może on mieć wiele, musisz zamówić połączoną tabelę:

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

W przeciwnym razie wiersz, który otrzymasz w złączeniu, będzie losowy (a ściślej, w zależności od tego, co db znajdzie najpierw).

Chris Moschini
źródło
Otóż ​​to! Wszelkie niegwarantowane relacje jeden do jednego.
it3xl
2

Zmieniając odpowiedź Marca Gravella w metodę rozszerzenia, zrobiłem następujące.

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

Chociaż zaakceptowana odpowiedź działa i jest dobra dla Linq na obiekty, wkurzyło mnie to, że zapytanie SQL nie jest zwykłym lewym zewnętrznym złączeniem.

Poniższy kod opiera się na projekcie LinkKit, który umożliwia przekazywanie wyrażeń i wywoływanie ich w zapytaniu.

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

Można go używać w następujący sposób

Table1.LeftOuterJoin(Table2, x => x.Key1, x => x.Key2, (x,y) => new { x,y});
Bob Vale
źródło
-1

Jest na to łatwe rozwiązanie

Wystarczy użyć .HasValue w swoim Select

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

Bardzo łatwe, nie wymaga dołączania do grupy ani niczego innego

Dale Fraser
źródło