web-dev-qa-db-fra.com

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

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 pourrais-je 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(???)
231
LaserJesus
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});
369
Marc Gravell

Comme cela semble être la question de facto SO pour les jointures externes gauches en utilisant la syntaxe method (extension), j'ai pensé ajouter une alternative à la réponse actuellement sélectionnée qui (dans mon expérience du moins) a été plus commun ce que je cherche

// 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 en utilisant un jeu de données simple (en supposant que nous joignons les 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 du jeu de données.

86
Ocelot20

La méthode de jointure de groupe n’est pas nécessaire pour joindre deux jeux 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 rejoindre la 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 se transforme correctement en SQL . Pour LINQ en objets, il est préférable de le joindre en utilisant GroupJoin comme il utilise en interne Lookup, mais si vous interrogez une base de données, ignorer GroupJoin est considéré comme performant.

Pour moi, Personlay est plus lisible que GroupJoin (). SelectMany ()

34

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

En améliorant la réponse d'Ocelot20, si vous avez une table dans laquelle vous souhaitez joindre 0 ou 1 ligne hors de la jointure externe, mais que vous pourriez 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, quelle que soit la base de données trouvée en premier).

4
Chris Moschini

Transformant 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));
}
1
Harley Waldstein

Tandis que la réponse acceptée fonctionne et qu'elle convient à Linq to Objects, elle m'a fait remarquer que la requête SQL n'était pas simplement une jointure droite gauche.

Le code suivant repose sur/ LinkKit Project qui vous permet de transmettre des expressions et de les appeler à 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});
0
Bob Vale

Il y a une solution facile à cela

Il suffit d'utiliser .HasValue dans votre sélection

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

Très facile, pas besoin de groupe ou quoi que ce soit d'autre

0
Dale Fraser