Entity Framework links Join

82

Wie ändere ich diese Abfrage, damit alle u.usergroups zurückgegeben werden?

from u in usergroups
from p in u.UsergroupPrices
select new UsergroupPricesList
{
UsergroupID = u.UsergroupID,
UsergroupName = u.UsergroupName,
Price = p.Price
};
Lasse Edsvik
quelle
1
vielleicht dies kann helfen. es war auf einer anderen Frage hier auf SO
Menahem

Antworten:

134

angepasst von MSDN, wie man Join mit EF 4 verlässt

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
quelle
2
Ich mag das besser als where gj.DefaultIfEmpty () am Ende, weil ich x in where oder select verwenden kann!
Gary
1
Können Sie die Zeile 'from x in gj.DefaultIfEmpty ()' erklären?
Alex Dresko
@AlexDresko Dieser Teil nimmt alle Ergebnisse aus dem Join und gibt für diejenigen, die keinen Wert für die rechte Hand haben, null an (Standard ist das Objekt null). hth
Menahem
2
Was ist, wenn mehr als zwei Tabellen vorhanden sind?
MohammadHossein R
1
Dies hat sich mit efcore leicht geändert. from x in gj.DefaultIfEmpty()wird from p in gj.DefaultIfEmpty(). docs.microsoft.com/en-us/ef/core/querying/…
carlin.scott
30

Es mag ein bisschen übertrieben sein, aber ich habe eine Erweiterungsmethode geschrieben, damit Sie LeftJoindie JoinSyntax verwenden können (zumindest in der Methodenaufrufnotation):

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

Mein Code fügt dem aktuellen Ausdrucksbaum lediglich ein GroupJoinund einen SelectManyAufruf hinzu . Trotzdem sieht es ziemlich kompliziert aus, da ich die Ausdrücke selbst erstellen und den vom Benutzer im resultSelectorParameter angegebenen Ausdrucksbaum ändern muss , damit der gesamte Baum von LINQ-to-Entities übersetzbar bleibt.

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
quelle
2
Danke für diese Erweiterung fero.
Fergers
Das ist immer noch großartig. Vielen Dank!
TheGeekYouNeed
1
Getestet in .NET Framework 4.6.2 und es funktioniert wie erwartet (dh generiert einen LEFT OUTER JOIN). Ich frage mich, ob es auf .NET Core funktioniert. Vielen Dank.
Alexei
23

Bitte erleichtern Sie sich das Leben (verwenden Sie nicht die Gruppe):

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
quelle
2
Das Vermeiden, einer Gruppe beizutreten, ist Ansichtssache, aber sicherlich eine gültige Meinung. Price = ugp.Pricekann fehlschlagen, wenn Pricees sich um eine nicht nullfähige Eigenschaft handelt und der linke Join keine Ergebnisse liefert.
1
Stimmen Sie dem oben Gesagten zu, aber mit mehr als zwei Tabellen ist dieser Ansatz viel einfacher zu lesen und zu pflegen.
Tomasz Skomra
1
Wir können prüfen, ob ugp == NULLund einen Standardwert für festlegen Price.
Hp93
einfach perfekt :)
MohammadHossein R
1
Genial! Ich bevorzuge diese Lösung aus Gründen der Lesbarkeit. Dies macht auch mehr Verknüpfungen (dh von 3 oder mehr Tabellen) viel einfacher! Ich habe es erfolgreich für 2 linke Joins (dh 3 Tabellen) verwendet.
Jeremy Morren
4

Wenn Sie die Methodenaufrufnotation bevorzugen, können Sie eine Linksverknüpfung in SelectManyKombination mit erzwingen DefaultIfEmpty. Zumindest auf Entity Framework 6, das SQL Server trifft. Zum Beispiel:

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

(Beachten Sie, dass dies MyTable2.Col2eine Spalte vom Typ ist int). Das generierte SQL sieht folgendermaßen aus:

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
quelle
Für mich erzeugt dies eine extrem langsame Abfrage mit "CROSS APPLY".
Meekohi
2

Für 2 und mehr linke Joins (linker Join von CreatorUser und InitiatorUser)

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
quelle
1

Ich konnte dies tun, indem ich DefaultIfEmpty () für das Hauptmodell aufrief. Dies erlaubte mir, bei faul geladenen Entitäten mitzumachen, was mir besser lesbar erscheint:

        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
quelle
1
Hier brauchen Sie überhaupt nicht .DefaultIfEmpty(): Es wirkt sich nur darauf aus, was passiert, wenn db.Complainses leer ist. würde db.Complains.Where(...).OrderBy(...).Select(x => new { ..., MemberName = x.Member != null ? x.Member.Name : string.Empty, ... })ohne einen .DefaultIfEmpty()bereits einen Link-Join ausführen (vorausgesetzt, die MemberEigenschaft ist als optional markiert).
1

Wenn UserGroups eine Eins-zu-Viele-Beziehung zur UserGroupPrices-Tabelle hat, wird in EF die Beziehung in Code wie folgt definiert:

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

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

Sie können die links verbundene Ergebnismenge einfach folgendermaßen abrufen:

var list = db.UserGroupDbSet.ToList();

Angenommen, Ihr DbSet für die linke Tabelle ist UserGroupDbSet, das die UserGrpPriceList enthält, eine Liste aller zugeordneten Datensätze aus der rechten Tabelle.

Jaggan_j
quelle