Entity framework left join


84

Come posso modificare questa query in modo che restituisca tutti i gruppi u.usergroup?

from u in usergroups
from p in u.UsergroupPrices
select new UsergroupPricesList
{
UsergroupID = u.UsergroupID,
UsergroupName = u.UsergroupName,
Price = p.Price
};

1
forse questo può aiutare. era su un'altra domanda qui su SO
Menahem

Risposte:


135

adattato da MSDN, come lasciare unirsi con 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) 
            };

2
Mi piace di più rispetto a dove gj.DefaultIfEmpty () alla fine perché posso usare x nel dove o selezionare!
Gary

1
Puoi spiegare la riga "from x in gj.DefaultIfEmpty ()"?
Alex Dresko

@AlexDresko questa parte prende tutti i risultati dal join e, per quelli che non hanno valore per la mano destra, ti dà null (il valore predefinito dell'oggetto è null). hth
Menahem

2
E se ci sono più di due tavoli?
MohammadHossein R

1
Questo è cambiato leggermente con efcore; from x in gj.DefaultIfEmpty()diventa from p in gj.DefaultIfEmpty(). docs.microsoft.com/en-us/ef/core/querying/…
carlin.scott

33

Potrebbe essere un po 'eccessivo, ma ho scritto un metodo di estensione, quindi puoi farlo LeftJoinusando la Joinsintassi (almeno nella notazione della chiamata del metodo):

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

Il mio codice non fa altro che aggiungere un file GroupJoin e una SelectManychiamata all'albero delle espressioni corrente. Tuttavia, sembra piuttosto complicato perché devo costruire le espressioni da solo e modificare l'albero delle espressioni specificato dall'utente nel resultSelectorparametro per mantenere l'intero albero traducibile da 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);
        }
    }
}

2
Grazie per questa estensione fero.
Fergers

Questo è ancora fantastico. Grazie!
TheGeekYouNeed

1
Testato in .NET Framework 4.6.2 e funziona come previsto (cioè genera un LEFT OUTER JOIN). Mi chiedo però se funziona su .NET Core. Grazie.
Alexei

24

Per favore renditi la vita più facile (non usare unisciti al gruppo):

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

2
Evitare di entrare in un gruppo è una questione di opinione, ma è certamente un'opinione valida. Price = ugp.Pricepotrebbe non riuscire se Priceè una proprietà non annullabile e il join sinistro non fornisce alcun risultato.

1
D'accordo con quanto sopra, ma con più di due tabelle questo approccio è molto più facile da leggere e mantenere.
Tomasz Skomra

1
Possiamo controllare se ugp == NULLe impostare un valore predefinito per Price.
Hp93

semplicemente perfetto :)
MohammadHossein R

1
Eccezionale! Preferisco questa soluzione per la leggibilità. Inoltre, questo rende molto più facili più join (cioè da 3 o più tavoli)! L'ho usato con successo per 2 left join (cioè 3 tabelle).
Jeremy Morren

4

Se preferisci la notazione della chiamata al metodo, puoi forzare un join sinistro usando SelectManycombinato con DefaultIfEmpty. Almeno su Entity Framework 6 che colpisce SQL Server. Per esempio:

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

(Nota che MyTable2.Col2è una colonna di tipo int). L'SQL generato sarà simile a questo:

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]

Per me questo sta generando una query estremamente lenta con "CROSS APPLY" in esso.
Meekohi

2

Per 2 e più join a sinistra (join a sinistra creatorUser e iniziatoreUser)

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

1

Sono stato in grado di farlo chiamando DefaultIfEmpty () sul modello principale. Questo mi ha permesso di lasciare il join su entità caricate pigre, mi sembra più leggibile:

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

1
Qui non serve .DefaultIfEmpty()affatto: influisce solo su ciò che accade quando db.Complainsè vuoto. db.Complains.Where(...).OrderBy(...).Select(x => new { ..., MemberName = x.Member != null ? x.Member.Name : string.Empty, ... }), senza alcuno .DefaultIfEmpty(), eseguirà già un join sinistro (supponendo che la Memberproprietà sia contrassegnata come facoltativa).

1

Se UserGroups ha una relazione uno a molti con la tabella UserGroupPrices, quindi in EF, una volta definita la relazione nel codice come:

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

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

Puoi estrarre il set di risultati unito a sinistra semplicemente in questo modo:

var list = db.UserGroupDbSet.ToList();

supponendo che il tuo DbSet per la tabella di sinistra sia UserGroupDbSet, che includerà UserGrpPriceList, che è un elenco di tutti i record associati dalla tabella di destra.

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.