如何将带有 LEFT JOIN 的 SQL 转换为 EF CORE 3 LINQ

Posted

技术标签:

【中文标题】如何将带有 LEFT JOIN 的 SQL 转换为 EF CORE 3 LINQ【英文标题】:How to convert SQL with LEFT JOIN to EF CORE 3 LINQ 【发布时间】:2021-08-05 20:19:35 【问题描述】:

我需要转换这条 SQL:

SELECT
  sb.Id AS id,
  sb.name as name,
  sb.age AS age,
  sb.BirthDay as BirthDay,
  su.UnitId AS unitId,
  sb.WorkedInDodo AS workedInDodo,
  sb.RelativesWorkedInDodo AS relativesWorkedInDodo,
  sb.PhoneNumber AS phoneNumber,
  sb.Email AS email,
  sb.DeliveryCheck AS deliveryCheck,
  sb.RestaurantCheck AS restaurantCheck,
  sb.StandardsRatingCheck as standardsRatingCheck,
  sb.SocialNetwork AS socialNetwork,
  sb.SocialNetworkId AS socialNetworkId,
  sb.socialNetworkScreenName AS socialNetworkScreenName,
  sb.SourceOfInformationAboutSecretBuyers AS SourceOfInformationAboutSecretBuyers,
  sb.suitable AS suitable,
  sb.SocialNetworkMessagingEnable as socialNetworkMessagingEnable,
  sb.IsInGroup as isInGroup,
  sb.IsKeyWord as IsKeyWord,
  sb.IsBanned as IsBanned,
  sb.IsFraud as IsFraud,
  sb.LastUnitsUpdateUtcDate as lastUnitsUpdateUtcDate,
  sb.Comments as comments,
  sb.MessagingApproval as messagingApproval,
  sb.ProcessDataApproval as processDataApproval,
  sb.CreatedDateTimeUtc AS createdDateTimeUtc,
  sb.ModifiedDateTineUTC AS modifiedDateTime,
  sb.Country AS country,
  sb.PhoneNumberEditedUtc AS PhoneNumberEditedUtc
  FROM (select * from
    (SELECT  s.*,
    IF(s.LastSearchMessageDateUtc is null, 0, 1) as searched,
    IF(cc.Id is null, 0, 1) as onCheck
    FROM secretbuyers s
    JOIN secretbuyerunits sbu ON sbu.SecretBuyerId = s.Id
    LEFT JOIN outgoingMessageQueue mq ON mq.Type = 1 AND mq.VkUserId = s.SocialNetworkId and mq.State in (1,2)
    LEFT JOIN checkcandidates cc ON cc.SecretBuyerId = s.id AND cc.State IN (1,4) AND cc.Date >= @p_checkBeginDateTime AND cc.Date <= @p_checkEndDateTime
    WHERE sbu.UnitId = @p_unitId
    AND (s.LastSearchMessageDateUtc is null OR s.LastSearchMessageDateUtc < @p_lastSearchMessageDateUtcLimit)
    AND ((@p_deliveryCheck = true AND s.DeliveryCheck = true) OR (@p_restaurantCheck = true AND s.RestaurantCheck = true) OR (@p_standardsRatingCheck = true AND s.StandardsRatingCheck = true))
    AND s.Suitable = true
    AND s.IsRemove = false
    AND mq.Id is null
    AND s.IsBanned = false
    AND s.IsFraud = false
    AND s.IsAutoSearchable = true
    group by s.Id
    Order By onCheck ASC, searched asc, s.LastSearchMessageDateUtc asc) as temp
    LIMIT @p_count) AS sb
  JOIN secretbuyerunits su ON su.SecretBuyerId = sb.Id;

到 EF Core 3.0 linq。主要问题是左连接。起初我尝试这样做:

private async Task<IEnumerable<MysteryShopper>> FindMysteryShoppers(
        SearchMysteryShoppersParameters searchParameters, CancellationToken ct)
    
        var lastSearchEdge = _nowProvider.UtcNow().Date.AddDays(-3);

        bool shouldHaveDeliveryCheckMark = ShouldHaveDeliveryCheckMark(searchParameters.SearchType);
        bool shouldHaveRestaurantCheckMark = ShouldHaveRestaurantCheckMark(searchParameters.SearchType);
        bool shouldHaveStandardsCheckMark = ShouldHaveStandardsCheckMark(searchParameters.SearchType);

        var lastCheckupEdgeDate = searchParameters.CheckupDates.Max().AddDays(-30);

        return await _ratingsContext.SecretBuyers.AsNoTracking().Where(ms => !ms.IsBanned &&
                !ms.IsFraud
                && ms.IsAutoSearchable
                && ms.Suitable
                && !ms.IsRemove
                && ms.SocialNetworkMessagingEnable
                && (ms.LastSearchMessageDateUtc == null
                    || ms.LastSearchMessageDateUtc < lastSearchEdge)
            )
            .Where(ms => !shouldHaveDeliveryCheckMark && ms.DeliveryCheck)
            .Where(ms => !shouldHaveRestaurantCheckMark && ms.RestaurantCheck)
            .Where(ms => !shouldHaveStandardsCheckMark && ms.StandardsRatingCheck)
            .Where(ms => ms.MysteryShopperUnits
                .Any(u => searchParameters.UnitIds.Contains(u.UnitId))
            )
            .GroupJoin(_ratingsContext.Checkups.AsNoTracking().Where(cc => !_cancelledStates.Contains(cc.State)), ms => ms.Id,
                cc => cc.SecretBuyerId, (ms, cc) => new
                
                    MysteryShopper = ms,
                    LastCheckupDate = cc
                        .Max(c => c.Date)
                
            )
            .GroupJoin(_ratingsContext.Messages.AsNoTracking().Where(m =>
                    m.Type == OutgoingMessageType.CandidateSearch
                    && (m.State == OutgoingMessageState.Sending || m.State == OutgoingMessageState.Waiting)),
                ms => ms.MysteryShopper.SocialNetworkId,
                m => m.VkUserId,
                (ms, m) =>
                    new ms.MysteryShopper, ms.LastCheckupDate, HasPendingMessages = m.Any())
            .Where(ms => !ms.HasPendingMessages)
            .Where(ms => ms.LastCheckupDate < lastCheckupEdgeDate)
            .OrderBy(ms => ms.LastCheckupDate != null)
            .ThenBy(ms => ms.MysteryShopper.LastSearchMessageDateUtc != null)
            .ThenBy(ms => ms.MysteryShopper.LastSearchMessageDateUtc)
            .Select(ms => ms.MysteryShopper)
            .ToArrayAsync(ct);
    

运行此查询后出现错误:

...'NavigationExpandingExpressionVisitor' 失败。这可能表示 EF Core 中的错误或限制。有关更多详细信息,请参阅https://go.microsoft.com/fwlink/?linkid=2101433。

在那之后,我从这个question 发现,您不能再根据在客户端上评估的查询进行分组。

经过更多搜索,我发现了另一个solution。所以我重新编写了这样的方法:

        private async Task<IEnumerable<MysteryShopper>> FindMysteryShoppers(
        SearchMysteryShoppersParameters searchParameters, CancellationToken ct)
    
        var lastSearchEdge = _nowProvider.UtcNow().Date.AddDays(-3);

        bool shouldHaveDeliveryCheckMark = ShouldHaveDeliveryCheckMark(searchParameters.SearchType);
        bool shouldHaveRestaurantCheckMark = ShouldHaveRestaurantCheckMark(searchParameters.SearchType);
        bool shouldHaveStandardsCheckMark = ShouldHaveStandardsCheckMark(searchParameters.SearchType);

        var lastCheckupEdgeDate = searchParameters.CheckupDates.Max().AddDays(-30);

        var mysteryShoppersLeftJoinedWithCheckups = from ms in _ratingsContext.Set<MysteryShopper>()
            where ms.MysteryShopperUnits
                .Any(u => searchParameters.UnitIds.Contains(u.UnitId)
                          && !shouldHaveStandardsCheckMark && ms.StandardsRatingCheck
                          && !shouldHaveRestaurantCheckMark && ms.RestaurantCheck
                          && !shouldHaveDeliveryCheckMark && ms.DeliveryCheck
                          && !ms.IsBanned
                          && !ms.IsFraud
                          && ms.IsAutoSearchable
                          && ms.Suitable
                          && !ms.IsRemove
                          && ms.SocialNetworkMessagingEnable
                          && (ms.LastSearchMessageDateUtc == null || ms.LastSearchMessageDateUtc < lastSearchEdge))
            join cc in _ratingsContext.Set<Checkup>()
                on ms.Id equals cc.SecretBuyerId into checkups
            from cc in checkups.DefaultIfEmpty()
            where !_cancelledStates.Contains(cc.State)
            select new MysteryShopper = ms, LastCheckupDate = checkups.Max(c => c.Date);


        var mysteryShoppersLeftJoinedWithCheckupsAndMessages =
            from mysteryShopperWithCheckup in mysteryShoppersLeftJoinedWithCheckups
            join m in _ratingsContext.Set<Message>()
                on mysteryShopperWithCheckup.MysteryShopper.SocialNetworkId equals m.VkUserId into messages
            from m in messages.DefaultIfEmpty()
            where m.Type == OutgoingMessageType.CandidateSearch && (m.State == OutgoingMessageState.Sending ||
                                                                    m.State == OutgoingMessageState.Waiting)
            select new
            
                mysteryShopperWithCheckup.MysteryShopper,
                mysteryShopperWithCheckup.LastCheckupDate,
                HasPendingMessages = messages.Any()
            ;

        var orderedMysteryShoppersWithoutPendingMessage =
                from mysteryShopperWithCheckupAndMessage in mysteryShoppersLeftJoinedWithCheckupsAndMessages
                where !mysteryShopperWithCheckupAndMessage.HasPendingMessages &&
                      mysteryShopperWithCheckupAndMessage.LastCheckupDate < lastCheckupEdgeDate
                orderby mysteryShopperWithCheckupAndMessage.LastCheckupDate != null,
                    mysteryShopperWithCheckupAndMessage.MysteryShopper.LastSearchMessageDateUtc != null,
                    mysteryShopperWithCheckupAndMessage.MysteryShopper.LastSearchMessageDateUtc
                select mysteryShopperWithCheckupAndMessage.MysteryShopper;

        return await orderedMysteryShoppersWithoutPendingMessage.ToArrayAsync(ct);
    

但现在我得到另一个错误:

无法翻译。以可翻译的形式重写查询,或通过插入对 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync() 的调用显式切换到客户端评估。

经过更多测试后,我确定 checkups.Max(c => c.Date) 和 HasPendingMessages = messages.Any() 是此错误的根源。如何修复此查询?您可以提出完全不同的解决方案或方法。主要思想是将sql中的逻辑引入c#。

PS:对不起,问题太长了。

【问题讨论】:

【参考方案1】:

添加了导航属性和掘金 Z.EntityFramework.Plus(用于 IncludeFilter)并像这样重写查询:

            var query = _ratingsContext.SecretBuyers
            .AsNoTracking()
            .IncludeFilter(ms => ms.Checkups.Where(cc => !_cancelledStates.Contains(cc.State)))
            .IncludeFilter(ms => ms.Messages.Where(m => m.Type == OutgoingMessageType.CandidateSearch &&
                                                        (m.State == OutgoingMessageState.Sending ||
                                                         m.State == OutgoingMessageState.Waiting)))
            .Where(ms => !ms.IsBanned
                         && !ms.IsFraud
                         && ms.IsAutoSearchable
                         && ms.Suitable
                         && !ms.IsRemove
                         && ms.SocialNetworkMessagingEnable
                         && (ms.LastSearchMessageDateUtc == null || ms.LastSearchMessageDateUtc < lastSearchEdge)
            )
            .Where(ms => !shouldHaveDeliveryCheckMark || ms.DeliveryCheck)
            .Where(ms => !shouldHaveRestaurantCheckMark || ms.RestaurantCheck)
            .Where(ms => !shouldHaveStandardsCheckMark || ms.StandardsRatingCheck)
            .Where(ms => ms.MysteryShopperUnits
                .Any(u => searchParameters.UnitIds.Contains(u.UnitId))
            )
            .Where(ms => !ms.Messages.Any())
            .Select(x => new MysteryShopper = x, LastCheckupDate = x.Checkups.Max(c => c.Date))
            .Where(ms => ms.LastCheckupDate < lastCheckupEdgeDate)
            .OrderBy(x => x.LastCheckupDate != null)
            .ThenBy(x => x.MysteryShopper.LastSearchMessageDateUtc != null)
            .ThenBy(x => x.MysteryShopper.LastSearchMessageDateUtc)
            .Select(x => x.MysteryShopper)
            .Take(searchParameters.MessagesPerUnit);

【讨论】:

以上是关于如何将带有 LEFT JOIN 的 SQL 转换为 EF CORE 3 LINQ的主要内容,如果未能解决你的问题,请参考以下文章

如何将 LEFT JOIN 限制为 SQL Server 中的第一个结果?

Linq-to-Entities:带有 WHERE 子句和投影的 LEFT OUTER JOIN

带有条件 CASE 语句的 SQL LEFT JOIN

SQL 2 Left Join 在一个带有计数的查询中

SQLAlchemy Left join WHERE 子句被转换为零和一

带有 WHERE 子句的 SQL INNER JOIN 到 LINQ 格式