LINQ to entities 中的 LEFT JOIN?

71

我正在尝试使用LINQ to Entities。

以下是我的问题: 我希望它能够执行以下操作:

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 

重复 - https://dev59.com/YHA75IYBdhLWcg3wPmZ8 - Anand
1
@Anand:不对,join总是产生内连接,没有条件的from是一个交叉连接,尽管被选为答案并获得了许多赞同,但它是错误和不足的。 - Stefan Steiger
可能是LEFT OUTER JOIN in LINQ的重复问题。 - Bartho Bernsmann
但是那里得到的最高票答案确实会生成左外连接,根据LinqPad的说法。(也许自2014年以来情况有所改变?)你在这里的答案以更简洁的方式完成了同样的事情,所以我喜欢它。 - General Grievance
7个回答

131

啊,我自己搞定了。
LINQ-2-entities的怪癖和技巧。
这看起来最容易理解:

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(),你就得到了一个内连接。
这正是我一直在寻找的。


46

你可以阅读我为LINQ中的连接编写的文章这里

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,但其他方面都正确。只是想知道当左连接超过两个表时应该如何操作。我一直在寻找更直观易懂的方法。最终我终于找到了 :) - Stefan Steiger
1
个人而言,我习惯使用扩展方法,并且非常喜欢它。如果您不断重复使用 GroupJoinSelectMany 这对方法,您可以得到一个很好(尽管有点冗长)的解决方案 :) - Giannis Paraskevopoulos

7
也许我稍后回答,但现在我正在面对这个问题...如果有帮助的话,还有一种解决方法(是我解决的方式)。
    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的代码,虽然也有帮助,但速度非常慢。


1
你是不是在使用 Linq-2-Objects 进行操作?因为它在这种情况下会很慢,因为它不使用索引。 - Stefan Steiger

6

使用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。

1
这正是我需要的5个表查询!!!非常感谢您发布这个,它为我节省了数小时的头痛! :) - Jamie
1
这是一种非常次优的连接方式。FirstOrDefault是一个材料化运算符,因此每次发出它时,实际上都会创建一个子查询。如果你嵌套这些东西,你将生成极其糟糕的SQL。"let"最适合内联变量或执行多对一的连接关系,其中调用连接不会扩展结果集。您还没有使用导航属性来帮助清理代码。这是更好的方式:from AItem in Db.A let BItem = AItem.B etc - K0D4

2

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

所以在lambda语法中,GroupJoin实际上是多余的。 SelectMany + DefaultIfEmpty映射也在官方dotnet/ef6存储库的一个测试用例中涵盖。请参见SelectMany_with_DefaultIfEmpty_translates_into_left_outer_join

SelectMany和其他JOIN

这里需要记住的最重要的事情是,SelectManyJoin在转换SQL JOIN时更加灵活。

  • 对于内连接,你甚至可以使用 SelectMany 代替 Join。只需删除 .DefaultIfEmpty()

  • 如果在 SelectMany 中也删除 Where,那么就可以执行 CROSS JOIN

    请参阅 如何使用 LINQ to SQL 执行 CROSS JOIN?

自定义扩展方法

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

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 背景,我认为这会使意图更加清晰。


1
你不仅可以在实体中使用这个,还可以在存储过程或其他数据源中使用:
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())'的想法,结果成功了! - Ricardo stands with Ukraine
不幸的是,创建实体会导致集成测试失败,因此不是一个好的解决方案。 - Ricardo stands with Ukraine

-1
左连接使用linq // System.Linq
        Test t = new Test();

        //t.Employees is employee List
        //t.EmployeeDetails is EmployeeDetail List

        var result = from emp in t.Employees
                     join ed in t.EmployeeDetails on emp.Id equals ed.EDId into tmp
                     from final in tmp.DefaultIfEmpty()
                     select new { emp.Id, emp.Name, final?.Address };

        foreach (var r in result)
        {
            Console.WriteLine($"Employee Id: {r.Id}, and Name: {r.Name}, and address is: {r.Address}");
        }

无需重复已有答案中提到的内容。 - Gert Arnold

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