¿Cómo se realiza una unión externa izquierda utilizando métodos de extensión linq

272

Suponiendo que tengo una combinación externa izquierda como tal:

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 }

¿Cómo expresaría la misma tarea usando métodos de extensión? P.ej

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

Respuestas:

445

Para una (externa izquierda) se unen de una mesa Barcon una mesa Fooen Foo.Foo_Id = Bar.Foo_Idla notación 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
fuente
27
En realidad, esto no es tan loco como parece. Básicamente GroupJoin, la unión externa izquierda, la SelectManyparte solo es necesaria dependiendo de lo que desee seleccionar.
George Mauer
66
Este patrón es excelente porque Entity Framework lo reconoce como una
unión
3
@nam Bueno, necesitaría una declaración where, x.Bar == null
Tod
2
@AbdulkarimKanaan sí - SelectMany aplana dos capas de 1-muchas en 1 capa con una entrada por par
Marc Gravell
1
@MarcGravell Le sugerí una edición para agregar un poco de explicación de lo que hizo en la fragmentación de su código.
B - rian
109

Dado que esta parece ser la pregunta SO de facto para las uniones externas izquierdas utilizando la sintaxis del método (extensión), pensé que agregaría una alternativa a la respuesta actualmente seleccionada que (al menos en mi experiencia) ha sido más comúnmente lo que soy despué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 });

Para mostrar la diferencia usando un conjunto de datos simple (suponiendo que nos unamos a los valores mismos):

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    }    

La opción 2 es fiel a la definición típica de combinación externa izquierda, pero como mencioné anteriormente, a menudo es innecesariamente compleja dependiendo del conjunto de datos.

Ocelot20
fuente
77
Creo que "bs.SingleOrDefault ()" no funcionará si tiene otro siguiente Seguir o Incluir. Necesitamos el "bs.FirstOrDefault ()" en estos casos.
Dherik
3
Es cierto que Entity Framework y Linq to SQL requieren que no puedan hacer la Singlecomprobación fácilmente en medio de una unión. SingleOrDefaultSin embargo, es una forma más "correcta" de demostrar esta OMI.
Ocelot20
1
Debe recordar Ordenar su tabla unida o .FirstOrDefault () obtendrá una fila aleatoria de las múltiples filas que pueden coincidir con los criterios de unión, lo que sea que la base de datos encuentre primero.
Chris Moschini
1
@ChrisMoschini: Order y FirstOrDefault son innecesarios ya que el ejemplo es para una coincidencia 0 o 1 en la que desearía fallar en varios registros (vea el comentario del código anterior).
Ocelot20
2
Este no es un "requisito adicional" no especificado en la pregunta, es lo que mucha gente piensa cuando dicen "Unión Exterior Izquierda". Además, el requisito de FirstOrDefault mencionado por Dherik es el comportamiento EF / L2SQL y no L2Objects (ninguno de estos está en las etiquetas). SingleOrDefault es absolutamente el método correcto para llamar en este caso. Por supuesto, desea lanzar una excepción si encuentra más registros de los posibles para su conjunto de datos en lugar de elegir uno arbitrario y generar un resultado confuso e indefinido.
Ocelot20
52

El método de unión de grupo no es necesario para lograr la unión de dos conjuntos de datos.

Unir internamente:

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

Para la unión izquierda simplemente agregue DefaultIfEmpty ()

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

EF y LINQ to SQL se transforman correctamente a SQL. Para LINQ to Objects es mejor unirse usando GroupJoin ya que internamente usa Lookup . Pero si está consultando DB, entonces omitir GroupJoin es AFAIK como performant.

Personlay para mí de esta manera es más legible en comparación con GroupJoin (). SelectMany ()

Gediminas Zimkus
fuente
Esto funcionó mejor que un .Join para mí, además de que podría hacer la articulación que quería (right.FooId == left.FooId || right.FooId == 0)
Anders
linq2sql traduce este enfoque como combinación izquierda. Esta respuesta es mejor y más simple. +1
Guido Mocha
15

Puede crear un método de extensión como:

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
fuente
Parece que funcionaría (para mi requerimiento). ¿Puede dar un ejemplo? Soy nuevo en LINQ Extensions y me está costando mucho entender esta situación de Left Join en la que estoy ...
Shiva
@Skychan Puede que necesite mirarlo, es una respuesta antigua y estaba funcionando en ese momento. ¿Qué marco estás usando? Me refiero a la versión .NET?
hajirazin
2
Esto funciona para Linq to Objects, pero no cuando se consulta una base de datos, ya que necesita operar en un IQuerable y usar Expressions of Funcs en su lugar
Bob Vale,
4

Mejorando la respuesta de Ocelot20, si tiene una tabla que le queda unida externamente donde solo desea 0 o 1 filas, pero podría tener varias, debe Ordenar su tabla unida:

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

De lo contrario, la fila que obtenga en la unión será aleatoria (o más específicamente, cualquiera que sea la base de datos que encuentre primero).

Chris Moschini
fuente
¡Eso es! Cualquier relación uno a uno no garantizada.
it3xl
2

Al convertir la respuesta de Marc Gravell en un método de extensión, hice lo siguiente.

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
fuente
2

Si bien la respuesta aceptada funciona y es buena para Linq to Objects, me molestó que la consulta SQL no sea solo una combinación externa izquierda.

El siguiente código se basa en el Proyecto LinkKit que le permite pasar expresiones e invocarlas a su consulta.

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

Se puede usar de la siguiente manera

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

Hay una solución fácil para esto

Simplemente use. HasValue en su Seleccionar

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

Muy fácil, no es necesario unirse al grupo ni nada más.

Dale Fraser
fuente