Entity framework left join

84

¿Cómo cambio esta consulta para que devuelva todos los u.usergroups?

from u in usergroups
from p in u.UsergroupPrices
select new UsergroupPricesList
{
UsergroupID = u.UsergroupID,
UsergroupName = u.UsergroupName,
Price = p.Price
};
Lasse Edsvik
fuente
1
tal vez esto pueda ayudar. estaba en otra pregunta aquí en SO
Menahem

Respuestas:

135

adaptado de MSDN, cómo unirse a la izquierda usando EF 4

var query = from u in usergroups
            join p in UsergroupPrices on u.UsergroupID equals p.UsergroupID into gj
            from x in gj.DefaultIfEmpty()
            select new { 
                UsergroupID = u.UsergroupID,
                UsergroupName = u.UsergroupName,
                Price = (x == null ? String.Empty : x.Price) 
            };
Menahem
fuente
2
Me gusta más esto que where gj.DefaultIfEmpty () al final porque puedo usar x en where o select!
Gary
1
¿Puede explicar la línea 'from x in gj.DefaultIfEmpty ()'?
Alex Dresko
@AlexDresko esta parte toma todos los resultados de la combinación, y para los que no tienen un valor a la derecha, le da un valor nulo (el objeto predeterminado es nulo). hth
Menahem
2
¿Y si hay más de dos mesas?
MohammadHossein R
1
Esto cambió ligeramente con efcore; from x in gj.DefaultIfEmpty()se convierte from p in gj.DefaultIfEmpty(). docs.microsoft.com/en-us/ef/core/querying/…
carlin.scott
30

Puede ser un poco exagerado, pero escribí un método de extensión, por lo que puede hacer un LeftJoinuso de la Joinsintaxis (al menos en la notación de llamada al método):

persons.LeftJoin(
    phoneNumbers,
    person => person.Id,
    phoneNumber => phoneNumber.PersonId,
    (person, phoneNumber) => new
        {
            Person = person,
            PhoneNumber = phoneNumber?.Number
        }
);

Mi código no hace más que agregar una GroupJoiny una SelectManyllamada al árbol de expresión actual. Sin embargo, parece bastante complicado porque tengo que construir las expresiones yo mismo y modificar el árbol de expresiones especificado por el usuario en el resultSelectorparámetro para mantener todo el árbol traducible por LINQ-to-Entities.

public static class LeftJoinExtension
{
    public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        MethodInfo groupJoin = typeof (Queryable).GetMethods()
                                                 .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] GroupJoin[TOuter,TInner,TKey,TResult](System.Linq.IQueryable`1[TOuter], System.Collections.Generic.IEnumerable`1[TInner], System.Linq.Expressions.Expression`1[System.Func`2[TOuter,TKey]], System.Linq.Expressions.Expression`1[System.Func`2[TInner,TKey]], System.Linq.Expressions.Expression`1[System.Func`3[TOuter,System.Collections.Generic.IEnumerable`1[TInner],TResult]])")
                                                 .MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), typeof (LeftJoinIntermediate<TOuter, TInner>));
        MethodInfo selectMany = typeof (Queryable).GetMethods()
                                                  .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] SelectMany[TSource,TCollection,TResult](System.Linq.IQueryable`1[TSource], System.Linq.Expressions.Expression`1[System.Func`2[TSource,System.Collections.Generic.IEnumerable`1[TCollection]]], System.Linq.Expressions.Expression`1[System.Func`3[TSource,TCollection,TResult]])")
                                                  .MakeGenericMethod(typeof (LeftJoinIntermediate<TOuter, TInner>), typeof (TInner), typeof (TResult));

        var groupJoinResultSelector = (Expression<Func<TOuter, IEnumerable<TInner>, LeftJoinIntermediate<TOuter, TInner>>>)
                                      ((oneOuter, manyInners) => new LeftJoinIntermediate<TOuter, TInner> {OneOuter = oneOuter, ManyInners = manyInners});

        MethodCallExpression exprGroupJoin = Expression.Call(groupJoin, outer.Expression, inner.Expression, outerKeySelector, innerKeySelector, groupJoinResultSelector);

        var selectManyCollectionSelector = (Expression<Func<LeftJoinIntermediate<TOuter, TInner>, IEnumerable<TInner>>>)
                                           (t => t.ManyInners.DefaultIfEmpty());

        ParameterExpression paramUser = resultSelector.Parameters.First();

        ParameterExpression paramNew = Expression.Parameter(typeof (LeftJoinIntermediate<TOuter, TInner>), "t");
        MemberExpression propExpr = Expression.Property(paramNew, "OneOuter");

        LambdaExpression selectManyResultSelector = Expression.Lambda(new Replacer(paramUser, propExpr).Visit(resultSelector.Body), paramNew, resultSelector.Parameters.Skip(1).First());

        MethodCallExpression exprSelectMany = Expression.Call(selectMany, exprGroupJoin, selectManyCollectionSelector, selectManyResultSelector);

        return outer.Provider.CreateQuery<TResult>(exprSelectMany);
    }

    private class LeftJoinIntermediate<TOuter, TInner>
    {
        public TOuter OneOuter { get; set; }
        public IEnumerable<TInner> ManyInners { get; set; }
    }

    private class Replacer : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParam;
        private readonly Expression _replacement;

        public Replacer(ParameterExpression oldParam, Expression replacement)
        {
            _oldParam = oldParam;
            _replacement = replacement;
        }

        public override Expression Visit(Expression exp)
        {
            if (exp == _oldParam)
            {
                return _replacement;
            }

            return base.Visit(exp);
        }
    }
}
fero
fuente
2
Gracias por esta extensión fero.
Fergers
Esto sigue siendo genial. ¡Gracias!
TheGeekYouNeed
1
Probé esto dentro de .NET Framework 4.6.2 y funciona como se esperaba (es decir, genera una LEFT OUTER JOIN). Sin embargo, me pregunto si funciona en .NET Core. Gracias.
Alexei
23

Por favor, haga su vida más fácil (no use unirse al grupo):

var query = from ug in UserGroups
            from ugp in UserGroupPrices.Where(x => x.UserGroupId == ug.Id).DefaultIfEmpty()
            select new 
            { 
                UserGroupID = ug.UserGroupID,
                UserGroupName = ug.UserGroupName,
                Price = ugp != null ? ugp.Price : 0 //this is to handle nulls as even when Price is non-nullable prop it may come as null from SQL (result of Left Outer Join)
            };
Tomasz Skomra
fuente
2
Evitar unirse a un grupo es una cuestión de opinión, pero ciertamente es una opinión válida. Price = ugp.Pricepuede fallar si Pricees una propiedad que no acepta valores NULL y, sin embargo, la combinación izquierda no da ningún resultado.
1
De acuerdo con lo anterior, pero con más de dos tablas, este enfoque es mucho más fácil de leer y mantener.
Tomasz Skomra
1
Podemos comprobar si ugp == NULLy establecer un valor predeterminado para Price.
Hp93
simplemente perfecto :)
MohammadHossein R
1
¡Increíble! Prefiero esta solución por legibilidad. Además, esto hace que más uniones (es decir, de 3 o más tablas) sean mucho más fáciles. Lo usé con éxito para 2 combinaciones izquierdas (es decir, 3 tablas).
Jeremy Morren
4

Si prefiere la notación de llamada al método, puede forzar una combinación a la izquierda usando SelectManycombinado con DefaultIfEmpty. Al menos en Entity Framework 6 golpeando SQL Server. Por ejemplo:

using(var ctx = new MyDatabaseContext())
{
    var data = ctx
    .MyTable1
    .SelectMany(a => ctx.MyTable2
      .Where(b => b.Id2 == a.Id1)
      .DefaultIfEmpty()
      .Select(b => new
      {
        a.Id1,
        a.Col1,
        Col2 = b == null ? (int?) null : b.Col2,
      }));
}

(Tenga en cuenta que MyTable2.Col2es una columna de tipo int). El SQL generado se verá así:

SELECT 
    [Extent1].[Id1] AS [Id1], 
    [Extent1].[Col1] AS [Col1], 
    CASE WHEN ([Extent2].[Col2] IS NULL) THEN CAST(NULL AS int) ELSE  CAST( [Extent2].[Col2] AS int) END AS [Col2]
    FROM  [dbo].[MyTable1] AS [Extent1]
    LEFT OUTER JOIN [dbo].[MyTable2] AS [Extent2] ON [Extent2].[Id2] = [Extent1].[Id1]
Diego
fuente
Para mí, esto está generando una consulta extremadamente lenta con "CROSS APPLY" en ella.
Meekohi
2

Para 2 y más combinaciones izquierdas (unión izquierda creatorUser e iniciatorUser)

IQueryable<CreateRequestModel> queryResult = from r in authContext.Requests
                                             join candidateUser in authContext.AuthUsers
                                             on r.CandidateId equals candidateUser.Id
                                             join creatorUser in authContext.AuthUsers
                                             on r.CreatorId equals creatorUser.Id into gj
                                             from x in gj.DefaultIfEmpty()
                                             join initiatorUser in authContext.AuthUsers
                                             on r.InitiatorId equals initiatorUser.Id into init
                                             from x1 in init.DefaultIfEmpty()

                                             where candidateUser.UserName.Equals(candidateUsername)
                                             select new CreateRequestModel
                                             {
                                                 UserName = candidateUser.UserName,
                                                 CreatorId = (x == null ? String.Empty : x.UserName),
                                                 InitiatorId = (x1 == null ? String.Empty : x1.UserName),
                                                 CandidateId = candidateUser.UserName
                                             };
Dmitrii Matunin
fuente
1

Pude hacer esto llamando a DefaultIfEmpty () en el modelo principal. Esto me permitió unirme a la izquierda en entidades cargadas de forma diferida, me parece más legible:

        var complaints = db.Complaints.DefaultIfEmpty()
            .Where(x => x.DateStage1Complete == null || x.DateStage2Complete == null)
            .OrderBy(x => x.DateEntered)
            .Select(x => new
            {
                ComplaintID = x.ComplaintID,
                CustomerName = x.Customer.Name,
                CustomerAddress = x.Customer.Address,
                MemberName = x.Member != null ? x.Member.Name: string.Empty,
                AllocationName = x.Allocation != null ? x.Allocation.Name: string.Empty,
                CategoryName = x.Category != null ? x.Category.Ssl_Name : string.Empty,
                Stage1Start = x.Stage1StartDate,
                Stage1Expiry = x.Stage1_ExpiryDate,
                Stage2Start = x.Stage2StartDate,
                Stage2Expiry = x.Stage2_ExpiryDate
            });
William Robinson
fuente
1
Aquí, no necesitas .DefaultIfEmpty()nada: solo afecta lo que sucede cuando db.Complainsestá vacío. db.Complains.Where(...).OrderBy(...).Select(x => new { ..., MemberName = x.Member != null ? x.Member.Name : string.Empty, ... }), sin ninguno .DefaultIfEmpty(), ya realizaría una combinación izquierda (suponiendo que la Memberpropiedad esté marcada como opcional).
1

Si UserGroups tiene una relación de uno a muchos con la tabla UserGroupPrices, entonces en EF, una vez que la relación se define en código como:

//In UserGroups Model
public List<UserGroupPrices> UserGrpPriceList {get;set;}

//In UserGroupPrices model
public UserGroups UserGrps {get;set;}

Puede extraer el conjunto de resultados unido a la izquierda simplemente de esta manera:

var list = db.UserGroupDbSet.ToList();

asumiendo que su DbSet para la tabla de la izquierda es UserGroupDbSet, que incluirá UserGrpPriceList, que es una lista de todos los registros asociados de la tabla de la derecha.

Jaggan_j
fuente