Come si esegue un join esterno sinistro usando i metodi di estensione linq


272

Supponendo di avere un join esterno sinistro in quanto tale:

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 }

Come esprimerei la stessa attività usando i metodi di estensione? Per esempio

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

Risposte:


445

Per un join (esterno sinistro) di una tabella Barcon una tabella Fooattiva Foo.Foo_Id = Bar.Foo_Idnella notazione 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});

27
Questo in realtà non è così folle come sembra. Fondamentalmente GroupJoinesegue il join esterno sinistro, la SelectManyparte è necessaria solo in base a ciò che si desidera selezionare.
George Mauer,

6
Questo modello è eccezionale perché Entity Framework lo riconosce come Left Join, che credevo fosse impossibile
Jesan Fafon,

3
@nam Beh, avresti bisogno di un'istruzione where, x.Bar == null
Tod

2
@AbdulkarimKanaan sì - SelectMany appiattisce due strati da 1 a molti in 1 strato con una voce per coppia
Marc Gravell

1
@MarcGravell Ho suggerito una modifica per aggiungere un po 'di spiegazione di ciò che hai fatto nel tuo frugging del codice.
B - rian,

109

Poiché questa sembra essere la domanda SO de facto per i join esterni a sinistra usando la sintassi del metodo (estensione), ho pensato di aggiungere un'alternativa alla risposta attualmente selezionata che (almeno nella mia esperienza) è stata più comunemente ciò che sono dopo

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

Per visualizzare la differenza utilizzando un semplice set di dati (supponendo che stiamo unendo i valori stessi):

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    }    

L'opzione 2 è vera per la tipica definizione del join esterno sinistro, ma come ho già detto in precedenza è spesso inutilmente complessa a seconda del set di dati.


7
Penso che "bs.SingleOrDefault ()" non funzionerà se hai un altro successivo Join o Include. Abbiamo bisogno di "bs.FirstOrDefault ()" in questi casi.
Dherik,

3
È vero, Entity Framework e Linq to SQL richiedono entrambi poiché non possono eseguire facilmente il Singlecontrollo in mezzo a un join. SingleOrDefaulttuttavia è un modo più "corretto" per dimostrare questo IMO.
Ocelot20,

1
Devi ricordare di Ordinare la tua tabella unita o .FirstOrDefault () otterrà una riga casuale da più righe che potrebbero corrispondere ai criteri di join, qualunque cosa accada prima di trovare il database.
Chris Moschini,

1
@ChrisMoschini: Order e FirstOrDefault non sono necessari poiché l'esempio è per una corrispondenza 0 o 1 in cui si vorrebbe fallire su più record (vedere il commento sopra il codice).
Ocelot20

2
Questo non è un "requisito aggiuntivo" non specificato nella domanda, è ciò che molte persone pensano quando dicono "Left Outer Join". Inoltre, il requisito FirstOrDefault a cui fa riferimento Dherik è il comportamento EF / L2SQL e non gli oggetti L2Ob (nessuno dei due si trova nei tag). SingleOrDefault è assolutamente il metodo corretto da chiamare in questo caso. Ovviamente vuoi lanciare un'eccezione se incontri più record del possibile per il tuo set di dati invece di sceglierne uno arbitrario e portare a un risultato confuso e indefinito.
Ocelot20

52

Il metodo Join di gruppo non è necessario per ottenere l'unione di due set di dati.

Inner Join:

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

Per Left Join basta aggiungere DefaultIfEmpty ()

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

EF e LINQ to SQL si trasformano correttamente in SQL. Per LINQ to Objects è meglio unirsi usando GroupJoin in quanto utilizza internamente Ricerca . Ma se stai interrogando DB, saltare di GroupJoin è AFAIK come performante.

Personlay per me in questo modo è più leggibile rispetto a GroupJoin (). SelectMany ()


Questo ha funzionato meglio di un .Jinin per me, in più ho potuto fare il mio giunto condizionale che volevo (right.FooId == left.FooId || right.FooId == 0)
Anders

linq2sql traduce questo approccio come join sinistro. questa risposta è migliore e più semplice. +1
Guido Mocha,

15

Puoi creare un metodo di estensione come:

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

Sembra che funzionerebbe (per il mio requisito). Potete fornire un esempio? Sono nuovo alle estensioni di LINQ e sto avendo difficoltà a avvolgere la mia testa attorno a questa situazione di sinistra. Sono in ...
Shiva,

@Skychan Potrebbe essere necessario guardarlo, è una vecchia risposta e funzionava in quel momento. Quale framework stai usando? Intendo la versione .NET?
hajirazin,

2
Questo funziona per Linq to Objects ma non quando si esegue una query su un database in quanto è necessario operare su un IQuerable e utilizzare invece Expressions of Funcs
Bob Vale

4

Migliorando la risposta di Ocelot20, se si dispone di una tabella che si desidera unire all'esterno con la quale si desidera solo 0 o 1 righe, ma potrebbe avere più, è necessario ordinare la tabella unita:

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

Altrimenti quale riga otterrai nel join sarà casuale (o più specificamente, qualunque sia il db che trova per primo).


Questo è tutto! Qualsiasi rapporto uno a uno non garantito.
it3xl,

2

Trasformando la risposta di Marc Gravell in un metodo di estensione, ho fatto quanto segue.

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

2

Mentre la risposta accettata funziona ed è utile per Linq to Objects, mi ha infastidito il fatto che la query SQL non sia solo un join esterno sinistro.

Il codice seguente si basa sul progetto LinkKit che consente di passare espressioni e invocarle alla query.

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

Può essere usato come segue

Table1.LeftOuterJoin(Table2, x => x.Key1, x => x.Key2, (x,y) => new { x,y});

-1

C'è una soluzione semplice a questo

Usa .HasValue nella tua selezione

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

Molto facile, non c'è bisogno di groupjoin o altro

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.