【问题标题】:LEFT JOIN in LINQ to entities?LINQ中的左连接到实体?
【发布时间】:2013-10-21 19:04:07
【问题描述】:

我正在试用 LINQ to entity。

我有以下问题: 我希望它这样做:

SELECT 
     T_Benutzer.BE_User
    ,T_Benutzer_Benutzergruppen.BEBG_BE
FROM T_Benutzer

LEFT JOIN T_Benutzer_Benutzergruppen
    ON T_Benutzer_Benutzergruppen.BEBG_BE = T_Benutzer.BE_ID 

我最接近的事情是这样的:

        var lol = (
            from u in Repo.T_Benutzer

            //where u.BE_ID == 1
            from o in Repo.T_Benutzer_Benutzergruppen.DefaultIfEmpty()
                // on u.BE_ID equals o.BEBG_BE

            where (u.BE_ID == o.BEBG_BE || o.BEBG_BE == null)

            //join bg in Repo.T_Benutzergruppen.DefaultIfEmpty()
            //    on o.BEBG_BG equals bg.ID

            //where bg.ID == 899 

            orderby
                u.BE_Name ascending
                //, bg.Name descending

            //select u 
            select new
            {
                 u.BE_User
                ,o.BEBG_BG
                //, bg.Name 
            }
         ).ToList();

但这会产生与内连接相同的结果,而不是左连接。
此外,它还创建了这个完全疯狂的 SQL:

SELECT 
     [Extent1].[BE_ID] AS [BE_ID]
    ,[Extent1].[BE_User] AS [BE_User]
    ,[Join1].[BEBG_BG] AS [BEBG_BG]
FROM  [dbo].[T_Benutzer] AS [Extent1]

CROSS JOIN  
(
    SELECT 
         [Extent2].[BEBG_BE] AS [BEBG_BE]
        ,[Extent2].[BEBG_BG] AS [BEBG_BG]
    FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
    LEFT OUTER JOIN [dbo].[T_Benutzer_Benutzergruppen] AS [Extent2] 
        ON 1 = 1 
) AS [Join1]

WHERE [Extent1].[BE_ID] = [Join1].[BEBG_BE] 
OR [Join1].[BEBG_BE] IS NULL

ORDER BY [Extent1].[BE_Name] ASC

如何在 LINQ-2-entities 中进行左连接 其他人仍然可以理解该代码中正在做什么?

最好是生成的 SQL 的样子:

SELECT 
     T_Benutzer.BE_User
    ,T_Benutzer_Benutzergruppen.BEBG_BE
FROM T_Benutzer

LEFT JOIN T_Benutzer_Benutzergruppen
    ON T_Benutzer_Benutzergruppen.BEBG_BE = T_Benutzer.BE_ID 

【问题讨论】:

标签: c# linq entity-framework entity-framework-4 linq-to-entities


【解决方案1】:

Lambda 语法映射

查询语法解决方案很好,但在某些情况下,lambda 语法解决方案更可取(例如,处理表达式树)。 LinqPad 方便地将查询语法转换为映射查询的 lambda 语法。稍作调整,我们最终得到:

// Left-join in query syntax (as seen in several other answers)
var querySyntax = 
  from o in dbcontext.Outer
  from i in dbcontext.Inner.Where(i => i.ID == o.ID).DefaultIfEmpty()
  select new { o.ID, i.InnerField };

// Maps roughly to:
var lambdaSyntax = dbcontext.Outer
    .SelectMany(
        o => dbcontext.Inner.Where(i => i.ID == o.ID).DefaultIfEmpty(),
        (o, i) => new { o.ID, i.InnerField }
    );

所以GroupJoin 在 lambda 语法中实际上是多余的。 SelectMany + DefaultIfEmpty 映射也包含在官方dotnet/ef6 repo 的测试用例之一中。见SelectMany_with_DefaultIfEmpty_translates_into_left_outer_join

SelectMany 和其他 JOINs

这里最重要的一点是,SelectMany 在翻译 SQL JOINs 时比 Join 更通用。

自定义扩展方法

使用上面的 Lambda 语句,我们可以在 lambda 语法中创建一个类似于Join 扩展方法:

public static class Ext
{
    // The extension method
    public static IQueryable<TResult> LeftOuterJoin<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)
    {
        // Re-context parameter references in key selector lambdas.
        // Will give scoping issues otherwise
        var oParam = Expression.Parameter(
            typeof(TOuter), 
            outerKeySelector.Parameters[0].Name
        );
        var iParam = Expression.Parameter(
            typeof(TInner), 
            innerKeySelector.Parameters[0].Name
        );
        
        var innerLinqTypeArgs = new Type[]{ typeof(TInner) };
        
        // Maps `inner.Where(i => outerKeySelector body == innerKeySelector body)`
        var whereCall = Expression.Call(
            typeof(Queryable), nameof(Queryable.Where), innerLinqTypeArgs,
            // Capture `inner` arg
            Expression.Constant(inner),
            (Expression<Func<TInner, bool>>)Expression.Lambda(
                SwapParams(
                    Expression.Equal(innerKeySelector.Body, outerKeySelector.Body),
                    new[] { iParam, oParam }
                ),
                iParam
            )
        );
        
        // Maps `(IEnumerable<TRight>)<Where Call>.DefaultIfEmpty()`
        // Cast is required to get SelectMany to work
        var dieCall = Expression.Convert(
            Expression.Call(typeof(Queryable), nameof(Queryable.DefaultIfEmpty), innerLinqTypeArgs, whereCall),
            typeof(IEnumerable<TInner>)
        );
        
        // Maps `o => <DefaultIfEmpty Call>`
        var innerLambda = (Expression<Func<TOuter, IEnumerable<TInner>>>)Expression.Lambda(dieCall, oParam);
        
        return outer.SelectMany(innerLambda, resultSelector);
    }
    
    // Core class used by SwapParams
    private class ParamSwapper : ExpressionVisitor
    {
        public ParameterExpression Replacement;
        
        // Replace if names match, otherwise leave alone.
        protected override Expression VisitParameter(ParameterExpression node)
            => node.Name == Replacement.Name ? Replacement : node;
    }
    
    // Swap out a lambda's parameter references for other parameters
    private static Expression SwapParams(Expression tgt, ParameterExpression[] pExps)
    {
        foreach (var pExp in pExps)
            tgt = new ParamSwapper { Replacement = pExp }.Visit(tgt);
            
        return tgt;
    }
}

示例用法:

dbcontext.Outer
    .LeftOuterJoin(
        dbcontext.Inner, o => o.ID, i => i.ID, 
        (o, i) => new { o.ID, i.InnerField }
    );

当然,它并不能节省大量的输入,但我认为如果您来自 SQL 背景,它确实可以让意图更加清晰。

【讨论】:

    【解决方案2】:

    简单的方法是使用let 关键字。这对我有用。

    from AItem in Db.A
    let BItem = Db.B.Where(x => x.id == AItem.id ).FirstOrDefault() 
    where SomeCondition
    select new YourViewModel
    {
        X1 = AItem.a,
        X2 = AItem.b,
        X3 = BItem.c
    }
    

    这是对左连接的模拟。如果B表中的每一项都与A项不匹配,BItem返回null

    【讨论】:

    • 这正是我的 5 表查询所需要的!!!非常感谢您发布此信息,它为我节省了数小时的头痛! :)
    【解决方案3】:

    您可以阅读我为 LINQ here 中的连接写的一篇文章

    var query = 
    from  u in Repo.T_Benutzer
    join bg in Repo.T_Benutzer_Benutzergruppen
        on u.BE_ID equals bg.BEBG_BE
    into temp
    from j in temp.DefaultIfEmpty()
    select new
    {
        BE_User = u.BE_User,
        BEBG_BG = (int?)j.BEBG_BG// == null ? -1 : j.BEBG_BG
                //, bg.Name 
    }
    

    以下是使用扩展方法的等价物:

    var query = 
    Repo.T_Benutzer
    .GroupJoin
    (
        Repo.T_Benutzer_Benutzergruppen,
        x=>x.BE_ID,
        x=>x.BEBG_BE,
        (o,i)=>new {o,i}
    )
    .SelectMany
    (
        x => x.i.DefaultIfEmpty(),
        (o,i) => new
        {
            BE_User = o.o.BE_User,
            BEBG_BG = (int?)i.BEBG_BG
        }
    );
    

    【讨论】:

    • 这是 T_Benutzer_Benutzergruppen,不是 T_Benutzergruppen,但在其他方面是正确的。只是想知道当加入两个以上的表时这应该如何工作。我一直在寻找一种更直观易懂的方法。我终于找到了:)
    • 我个人习惯了扩展方法之一,我真的很喜欢它。如果你不断重复GroupJoinSelectMany 对,你可以有一个不错的,虽然很长,解决方案:)
    【解决方案4】:

    也许我稍后会来回答,但现在我正面临这个问题......如果有帮助,还有另一种解决方案(我解决它的方式)。

        var query2 = (
        from users in Repo.T_Benutzer
        join mappings in Repo.T_Benutzer_Benutzergruppen on mappings.BEBG_BE equals users.BE_ID into tmpMapp
        join groups in Repo.T_Benutzergruppen on groups.ID equals mappings.BEBG_BG into tmpGroups
        from mappings in tmpMapp.DefaultIfEmpty()
        from groups in tmpGroups.DefaultIfEmpty()
        select new
        {
             UserId = users.BE_ID
            ,UserName = users.BE_User
            ,UserGroupId = mappings.BEBG_BG
            ,GroupName = groups.Name
        }
    
    );
    

    顺便说一句,我尝试使用 Stefan Steiger 代码,它也有帮助,但速度慢得要命。

    【讨论】:

    • 您是否有机会在 Linq-2-Objects 中这样做?因为它会很慢,因为它不使用索引。
    【解决方案5】:

    啊,我自己搞定了。
    LINQ-2 实体的怪癖和夸克。
    这看起来最容易理解:

    var query2 = (
        from users in Repo.T_Benutzer
        from mappings in Repo.T_Benutzer_Benutzergruppen
            .Where(mapping => mapping.BEBG_BE == users.BE_ID).DefaultIfEmpty()
        from groups in Repo.T_Benutzergruppen
            .Where(gruppe => gruppe.ID == mappings.BEBG_BG).DefaultIfEmpty()
        //where users.BE_Name.Contains(keyword)
        // //|| mappings.BEBG_BE.Equals(666)  
        //|| mappings.BEBG_BE == 666 
        //|| groups.Name.Contains(keyword)
    
        select new
        {
             UserId = users.BE_ID
            ,UserName = users.BE_User
            ,UserGroupId = mappings.BEBG_BG
            ,GroupName = groups.Name
        }
    
    );
    
    
    var xy = (query2).ToList();
    

    删除.DefaultIfEmpty(),您将获得一个内部连接。
    这就是我想要的。

    【讨论】:

      【解决方案6】:

      您不仅可以在实体中使用它,还可以在存储过程或其他数据源中使用它:

      var customer = (from cus in _billingCommonservice.BillingUnit.CustomerRepository.GetAll()  
                                join man in _billingCommonservice.BillingUnit.FunctionRepository.ManagersCustomerValue()  
                                on cus.CustomerID equals man.CustomerID  
                                // start left join  
                                into a  
                                from b in a.DefaultIfEmpty(new DJBL_uspGetAllManagerCustomer_Result() )  
                                select new { cus.MobileNo1,b.ActiveStatus });  
      

      【讨论】:

      • 使用 DefaultIfEmpty 模拟查询时出错。从这里得到了创建一个默认类'a.DefaultIfEmpty(new DJBL_uspGetAllManagerCustomer_Result())'的想法,它奏效了!
      • 不幸的是,创建实体会使集成测试失败,所以不是一个好的解决方案。
      猜你喜欢
      • 2010-12-03
      • 2015-01-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-03
      • 2013-05-15
      相关资源
      最近更新 更多