Comment effectuer une jointure externe gauche à l'aide des méthodes d'extension linq

272

En supposant que j'ai une jointure externe gauche en tant que telle:

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 }

Comment exprimer la même tâche en utilisant des méthodes d'extension? Par exemple

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

Réponses:

445

Pour un (extérieur gauche) se joignent d'une table Baravec une table Foosur Foo.Foo_Id = Bar.Foo_Idla notation 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
la source
27
Ce n'est en fait pas aussi fou qu'il y paraît. Fondamentalement, GroupJoinla jointure externe gauche est utilisée, la SelectManypièce n'est nécessaire qu'en fonction de ce que vous souhaitez sélectionner.
George Mauer
6
Ce modèle est génial car Entity Framework le reconnaît comme une jointure gauche, ce que je
croyais auparavant
3
@nam Eh bien, vous auriez besoin d'une instruction where, x.Bar == null
Tod
2
@AbdulkarimKanaan yes - SelectMany aplatit deux couches de 1-many en 1 couche avec une entrée par paire
Marc Gravell
1
@MarcGravell J'ai suggéré une modification pour ajouter un peu d'explication de ce que vous avez fait dans votre gestion de code.
B - rian
109

Étant donné que cela semble être la question de facto SO pour les jointures externes gauches à l'aide de la syntaxe de la méthode (extension), j'ai pensé ajouter une alternative à la réponse actuellement sélectionnée qui (dans mon expérience au moins) a été plus communément ce que je suis après

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

Pour afficher la différence à l'aide d'un simple ensemble de données (en supposant que nous nous joignons aux valeurs elles-mêmes):

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'option 2 est fidèle à la définition typique de jointure externe gauche, mais comme je l'ai mentionné précédemment, elle est souvent inutilement complexe en fonction de l'ensemble de données.

Ocelot20
la source
7
Je pense que "bs.SingleOrDefault ()" ne fonctionnera pas si vous avez un autre Join ou Include suivant. Nous avons besoin du "bs.FirstOrDefault ()" dans ce cas.
Dherik
3
Certes, Entity Framework et Linq to SQL exigent tous les deux qu'ils ne peuvent pas facilement faire la Singlevérification au milieu d'une jointure. SingleOrDefaultest cependant une manière plus "correcte" de démontrer cette OMI.
Ocelot20
1
Vous devez vous rappeler de commander votre table jointe ou le .FirstOrDefault () va obtenir une ligne aléatoire à partir des plusieurs lignes qui pourraient correspondre aux critères de jointure, quel que soit la base de données à trouver en premier.
Chris Moschini du
1
@ChrisMoschini: Order et FirstOrDefault ne sont pas nécessaires car l'exemple concerne une correspondance 0 ou 1 où vous voudriez échouer sur plusieurs enregistrements (voir le code ci-dessus).
Ocelot20
2
Ce n'est pas une "exigence supplémentaire" non spécifiée dans la question, c'est ce que beaucoup de gens pensent quand ils disent "Left Outer Join". En outre, l'exigence FirstOrDefault à laquelle Dherik fait référence est le comportement EF / L2SQL et non L2Objects (aucun de ces éléments n'est dans les balises). SingleOrDefault est absolument la bonne méthode à appeler dans ce cas. Bien sûr, vous souhaitez lever une exception si vous rencontrez plus d'enregistrements que possible pour votre ensemble de données au lieu d'en choisir un arbitraire et de conduire à un résultat indéfini déroutant.
Ocelot20
52

La méthode Group Join n'est pas nécessaire pour réaliser la jonction de deux ensembles de données.

Jointure interne:

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

Pour la jointure gauche, ajoutez simplement DefaultIfEmpty ()

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

EF et LINQ to SQL se transforment correctement en SQL. Pour LINQ to Objects, il est préférable de se joindre à l'aide de GroupJoin car il utilise en interne la recherche . Mais si vous interrogez DB, le saut de GroupJoin est AFAIK comme performant.

Personlay pour moi de cette façon est plus lisible que GroupJoin (). SelectMany ()

Gediminas Zimkus
la source
Cela s'est mieux passé qu'un .Join pour moi, en plus je pouvais faire mon articulation conditionnelle que je voulais (right.FooId == left.FooId || right.FooId == 0)
Anders
linq2sql traduit cette approche par jointure gauche. cette réponse est meilleure et plus simple. +1
Guido Mocha
15

Vous pouvez créer une méthode d'extension comme:

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
la source
Il semble que cela fonctionnerait (pour mes besoins). Pouvez vous donner un exemple? Je suis nouveau dans les extensions LINQ et j'ai du mal à comprendre ce problème de jointure gauche dans lequel je suis ...
Shiva
@Skychan Peut-être que j'ai besoin de le regarder, c'est une vieille réponse et fonctionnait à l'époque. Quel cadre utilisez-vous? Je veux dire la version .NET?
hajirazin
2
Cela fonctionne pour Linq to Objects mais pas lors de l'interrogation d'une base de données car vous devez opérer sur un IQuerable et utiliser à la place des expressions de fonctions
Bob Vale
4

Améliorant la réponse d'Ocelot20, si vous avez une table qui vous reste à l'extérieur avec où vous voulez juste 0 ou 1 lignes, mais elle peut en avoir plusieurs, vous devez Commander votre table jointe:

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

Sinon, la ligne que vous obtenez dans la jointure sera aléatoire (ou plus précisément, selon la base de données qui se trouve en premier).

Chris Moschini
la source
C'est tout! Toute relation un à un non garantie.
it3xl
2

Transformer la réponse de Marc Gravell en une méthode d'extension, j'ai fait ce qui suit.

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
la source
2

Bien que la réponse acceptée fonctionne et soit bonne pour Linq to Objects, cela m'a dérangé que la requête SQL ne soit pas simplement une jointure externe gauche droite.

Le code suivant s'appuie sur le projet LinkKit qui vous permet de passer des expressions et de les invoquer dans votre requête.

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

Il peut être utilisé comme suit

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

Il existe une solution simple à ce problème

Utilisez simplement .HasValue dans votre Select

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

Très facile, pas besoin de groupjoin ou autre chose

Dale Fraser
la source