实体框架左连接

108

我该如何修改这个查询语句以返回所有的用户组(u.usergroups)?

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

1
也许这个链接可以帮到你:http://geekswithblogs.net/SudheersBlog/archive/2009/06/11/132758.aspx。它是在 Stack Overflow 上另一个问题的回答中提到的:http://stackoverflow.com/questions/2376701/linq-to-entities-how-to-define-left-join-for-grouping。 - Menahem
7个回答

167

本文改编自MSDN的《如何使用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
我更喜欢这个比在结尾处 gj.DefaultIfEmpty()因为我可以在where或select中使用x! - Gary
1
你能解释一下 'from x in gj.DefaultIfEmpty()' 这行代码的意思吗? - Alex Dresko
1
@AlexDresko,我同意它有些笨重。我猜这是因为组合连接操作并不像SQL连接那样,它返回的是分层结果而不是平面表格。 - Menahem
3
如果有超过两个表怎么办? - MohammadHossein R
3
这在 efcore 中略有改变;from x in gj.DefaultIfEmpty() 变成了 from p in gj.DefaultIfEmpty()。https://learn.microsoft.com/en-us/ef/core/querying/complex-query-operators#left-join - carlin.scott
显示剩余5条评论

49

这可能有点过度,但我写了一个扩展方法,因此您可以使用Join语法(至少在方法调用符号中)进行LeftJoin

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

我的代码除了在当前表达式树中添加一个GroupJoin和一个SelectMany调用之外,不会做任何其他事情。尽管如此,它看起来非常复杂,因为我必须自己构建表达式并修改由用户在resultSelector参数中指定的表达式树,以便整个树可被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);
        }
    }
}

4
感谢这个插件,fero。 - Fergers
这仍然很棒。谢谢! - TheGeekYouNeed
1
已在.NET Framework 4.6.2中进行了测试,按预期工作(即生成LEFT OUTER JOIN)。不过我想知道它是否也适用于.NET Core。谢谢。 - Alexei - check Codidact
为什么 ef 不默认使用左连接真是让人难以理解。许多数据库引擎会对另一个表执行整个表扫描,即使从主表中没有返回任何行。 - jjxtra
2
我确认在EF Core中这个可以工作。 - billy

48
请让您的生活更加轻松(不要使用加入群组的方式):
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)
            };

3
避免加入组是一种观点,但这确实是一个有效的观点。如果“Price”是非空属性且左连接没有返回任何结果,则“Price = ugp.Price”可能会失败。 - user743382
4
同意上述观点,但是如果有超过两个表格,这种方法会更容易阅读和维护。 - Tomasz Skomra
2
我们可以检查 ugp == NULL 并为 Price 设置默认值。 - Hp93
只是完美 :) - MohammadHossein R
3
太棒了!我更喜欢这个解决方案,因为它更易读。此外,这也使得更多联接(即来自3个或更多表格的联接)变得更加容易!我已经成功地使用它进行了2个左联接(即3个表格)。 - Jeremy Morren
如果我们想要从右表中获取一个列表呢?而不是像价格那样只获取单个值。 - MOH3N

7
如果您更喜欢使用方法调用符号,可以使用SelectManyDefaultIfEmpty结合来强制执行左连接。至少在Entity Framework 6中,命中SQL Server。例如:
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,
      }));
}

(请注意MyTable2.Col2是一个类型为int的列)生成的SQL将如下所示:
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]

对我来说,这个查询中使用了“CROSS APPLY”,导致查询非常缓慢。 - Meekohi
1
非常感谢,帮了我很多忙。 - morteza jafari

3

当进行2个或更多次左连接(在创建用户和发起人用户之间进行左连接)时

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

2

我可以通过在主模型上调用DefaultIfEmpty()来实现这一点。这使我能够在惰性加载的实体上进行左连接,对我来说似乎更易读:

        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
在这里,你根本不需要使用.DefaultIfEmpty():它只会影响当db.Complains为空时发生的情况。如果没有任何.DefaultIfEmpty()db.Complains.Where(...).OrderBy(...).Select(x => new { ..., MemberName = x.Member != null ? x.Member.Name : string.Empty, ... })已经执行了左连接(假设Member属性被标记为可选)。 - user743382

1
如果UserGroups和UserGroupPrices表之间存在一对多的关系,在EF中,一旦在代码中定义了此关系,如下所示:

如果UserGroups和UserGroupPrices表之间存在一对多的关系,在EF中,一旦在代码中定义了此关系,如下所示:

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

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

你可以通过简单地这样做来提取左连接结果集:

var list = db.UserGroupDbSet.ToList();

假设您的左表的DbSet为UserGroupDbSet,其中包括UserGrpPriceList,它是来自右表的所有相关记录的列表。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接