关于为啥这个 linq ef 核心查询不会执行的建议

Posted

技术标签:

【中文标题】关于为啥这个 linq ef 核心查询不会执行的建议【英文标题】:Suggestions on why this linq ef core query wont execute关于为什么这个 linq ef 核心查询不会执行的建议 【发布时间】:2021-12-23 03:44:41 【问题描述】:

早上好,

如果我 not 在 where 子句中添加 createdtimestamp 的条件,我在 EF Core 中有以下查询。如果我添加其他条件子句来过滤数据,如下所示,查询毫无问题地执行。

            var q = (
                from trckhead in DbContext.TrackingBatchHeader
                from userdets in DbContext.UserDetails
                    .Where(u => u.UserId == trckhead.ClosedByUserId).DefaultIfEmpty()
                from trckkeys in DbContext.TrackingBatchesItemKey
                    .Where(t => t.TrackingNo == trckhead.TrackingNo).DefaultIfEmpty()
                from trcklink1 in DbContext.TrackingBatchesLink
                    .Where(x => x.TrackingNo == trckhead.TrackingNo && x.TrackingType == "I").DefaultIfEmpty()
                from trcklink2 in DbContext.TrackingBatchesLink
                    .Where(x => x.TrackingNo == trckhead.TrackingNo && x.TrackingType == "T").DefaultIfEmpty()
                join trckref in DbContext.TrackingBatchesReference on trckhead.TrackingBatchType equals trckref
                    .TrackingBatchType
                join mthsite in DbContext.SiteDetail on trckhead.SiteCode equals mthsite.SiteCode
                join userdets2 in DbContext.UserDetails on trckhead.CreatedByUserId equals userdets2.UserId
                where
                (string.IsNullOrEmpty(searchRequest.Status) || trckhead.Status == searchRequest.Status) &&      
                trckhead.CreatedTimestamp >= DateTime.Now.AddMonths(-11)
                select new TrackingBatchSearchResultDto
                
                    TrackingBatchId = trckhead.TrackingNo,
                    SiteCode = trckhead.SiteCode,
                    TrackingBatchType = trckhead.TrackingBatchType,
                    Status = trckhead.Status,
                    Created = trckhead.CreatedTimestamp,
                    CreatedById = trckhead.CreatedByUserId,
                    ClosedById = trckhead.ClosedByUserId,
                    UserDescription = trckhead.UsersDescription,
                    TrackingBatchDescription = trckref.TrackingBatchTypeDescription,
                    SiteName = mthsite.Title,
                    CreatedByName = userdets2.FullUserName ?? string.Empty,
                    ClosedByName = userdets.FullUserName ?? string.Empty,
                    Link1 = trcklink1.TrackingData ?? string.Empty,
                    Link2 = trcklink2.TrackingData ?? string.Empty
                ).Distinct();

            return await q.ToListAsync();

如果我在查询中保留日期子句,则引发的异常显示以下内容。如果我将一个函数传递给查询,但检查一个简单的日期,我会认为服务器评估不会很高兴。我可以在获得整个数据集后按日期过滤结果,但返回的记录数量可能很大,所以我宁愿从数据库查询中过滤掉它。任何帮助将不胜感激。

The LINQ expression 'DbSet<TrackingBatchHeader>
    .SelectMany(
        source: t => DbSet<UserDetail>
            .Where(u => u.UserId == t.ClosedByUserId)
            .DefaultIfEmpty(), 
        collectionSelector: (t, c) => new TransparentIdentifier<TrackingBatchHeader, UserDetail>(
            Outer = t, 
            Inner = c
        ))
    .SelectMany(
        source: ti => DbSet<TrackingBatchesItemKey>
            .Where(t0 => t0.TrackingNo == ti.Outer.TrackingNo)
            .DefaultIfEmpty(), 
        collectionSelector: (ti, c) => new TransparentIdentifier<TransparentIdentifier<TrackingBatchHeader, UserDetail>, TrackingBatchesItemKey>(
            Outer = ti, 
            Inner = c
        ))
    .SelectMany(
        source: ti0 => DbSet<TrackingBatchesLink>
            .Where(t1 => t1.TrackingNo == ti0.Outer.Outer.TrackingNo && t1.TrackingType == "I")
            .DefaultIfEmpty(), 
        collectionSelector: (ti0, c) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TrackingBatchHeader, UserDetail>, TrackingBatchesItemKey>, TrackingBatchesLink>(
            Outer = ti0, 
            Inner = c
        ))
    .SelectMany(
        source: ti1 => DbSet<TrackingBatchesLink>
            .Where(t2 => t2.TrackingNo == ti1.Outer.Outer.Outer.TrackingNo && t2.TrackingType == "T")
            .DefaultIfEmpty(), 
        collectionSelector: (ti1, c) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TrackingBatchHeader, UserDetail>, TrackingBatchesItemKey>, TrackingBatchesLink>, TrackingBatchesLink>(
            Outer = ti1, 
            Inner = c
        ))
    .Join(
        outer: DbSet<TrackingBatchesReference>, 
        inner: ti2 => ti2.Outer.Outer.Outer.Outer.TrackingBatchType, 
        outerKeySelector: t3 => t3.TrackingBatchType, 
        innerKeySelector: (ti2, t3) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TrackingBatchHeader, UserDetail>, TrackingBatchesItemKey>, TrackingBatchesLink>, TrackingBatchesLink>, TrackingBatchesReference>(
            Outer = ti2, 
            Inner = t3
        ))
    .Join(
        outer: DbSet<SiteDetail>, 
        inner: ti3 => ti3.Outer.Outer.Outer.Outer.Outer.SiteCode, 
        outerKeySelector: s => s.SiteCode, 
        innerKeySelector: (ti3, s) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TrackingBatchHeader, UserDetail>, TrackingBatchesItemKey>, TrackingBatchesLink>, TrackingBatchesLink>, TrackingBatchesReference>, SiteDetail>(
            Outer = ti3, 
            Inner = s
        ))
    .Join(
        outer: DbSet<UserDetail>, 
        inner: ti4 => ti4.Outer.Outer.Outer.Outer.Outer.Outer.CreatedByUserId, 
        outerKeySelector: u0 => u0.UserId, 
        innerKeySelector: (ti4, u0) => new TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TransparentIdentifier<TrackingBatchHeader, UserDetail>, TrackingBatchesItemKey>, TrackingBatchesLink>, TrackingBatchesLink>, TrackingBatchesReference>, SiteDetail>, UserDetail>(
            Outer = ti4, 
            Inner = u0
        ))
    .Where(ti5 => ti5.Outer.Outer.Outer.Outer.Outer.Outer.Outer.CreatedTimestamp >= (Nullable<DateTime>)DateTime.Now.AddMonths(-11))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

【问题讨论】:

不要写这样的查询。 LINQ 不是 SQL 的替代品,EF Core 也不是数据访问库。 DbContext 不是数据库的模型。 EF Core 是一个对象到关系映射器。 EF Core 将根据对象之间的关系生成 SQL 查询。 JOIN 在 LINQ 查询中不应该有任何理由。至于Where 子句,不需要像string.IsNullOrEmpty(searchRequest.Status) 这样的包罗万象的技巧。您可以有条件地添加过滤器,例如if (status!=null) query=query.Where(t=&gt;t.Status == status); 如果您的 DbContext 和实体配置正确,您将不需要任何这些连接。如果您在 Select() 子句中使用简单的对象访问表示法,LINQ 会将其转换为必要的 JOIN 并在 SELECT 子句中添加必要的字段 异常与您发布的查询不匹配。你能简化查询并发布实际的查询和异常吗? @K4E 你应该。因为这就是简化查询和消除可能导致问题的代码所需要的。在那之后,包罗万象的条款是另一件可能导致问题的事情。 String.IsNullOrWhitespace 无法转换为 SQL。没有这样的功能。它甚至不必在查询中,因为它所做的只是禁用下一个子句。您可以链接.Where() 调用,结果等效于AND。这就是为什么使用if(status==null) q=q.Where(...);可以有条件地添加过滤器 @K4E 请忽略一般 cmets,因为它们与问题无关。您使用的是什么 EF Core 版本?因为最新的 EFC 5 / 6 在这种情况下会显示更好的错误消息,其中包含更多可能导致错误的提示。据我所知,问题要么是DateTime.Now.AddMonths(-11),我很确定它是可翻译的,要么很可能是未映射实体类的CreatedTimestamp 属性(通过[NotMapped].Ignore)。 【参考方案1】:

简而言之,忽略为实体之间的关系建立导航属性,而是使用“from”手动外连接加上 Distinct 对格式不正确的查询添加终极侮辱,试图像锤子一样使用 EF一个方钉穿过一个圆孔。

有了适当的关系,该查询应该是微不足道的。

您的日期时间条件错误消息与您的查询不匹配。

** 编辑:删除了有关日期比较的详细信息 - 这似乎没有直接关系,因为我确实验证了可空日期/日期时间比较确实适用于 EF Core 中的.AddMonth()

扩展条件逻辑有助于简化正在执行的查询。

例如:

var query = (/*build your base query*/);

if (!string.IsNullOrEmpty(searchRequest.Status))
    query = query.Where(x => x.Status == searchRequest.Status);
if (searchRequest.DateFrom.HasValue)

   var fromDate = searchRequest.DateFrom.Value.AddMonths(-11);
   query = query.Where(x => x.CreatedTimestamp >= fromDate);


var results = query.Select(...).Distinct().ToList();

只要您提供足够的信息来正确执行此操作,EF 就可以执行一些魔术来构建查询。看到它构建类似ti5.Outer.Outer.Outer.Outer.Outer.Outer.Outer.CreatedTimestamp 的东西应该会敲响警钟。您的查询中可能有一些潜伏的东西正在触发客户端评估,或者只是连接等的组合已经超过了复杂性限制或暴露了 EF Core 中的错误。出发点是通过利用导航属性而不是连接来简化查询表达式,并尽可能将条件逻辑外部化。

如果您别无选择,只能尝试查询您不准备使用导航属性正确映射的关系,我建议您探索在 SQL 中将查询构建为存储过程,然后定义一个可以映射到的实体该 Sproc 的结果记录。

【讨论】:

EF Core 与您习惯的 EF6 完全不同。虽然我也更喜欢/推荐导航属性而不是手动连接,但我绝不会建议使用 SP/原始 SQL 进行简单的连接查询。您提到的其他内容也不适用于 EFC - 条件 Where 不是强制性的,因为 EFC 消除了常量谓词(如您所见,错误表达式中没有 searchRequest.Status 条件)。同样根据他们使用的错误DateTime.Now.AddMonths(-11),这是完全可翻译的。所以你不知道具体的问题是什么,只是猜测。 当表达式访问者无法替换表达式时,我不喜欢 EF Core 抛出“无法翻译”的方式。虽然这对于 EF Core 团队来说可能更容易编写,但它无法提供有关失败发生原因的任何线索。您可能对日期表达式是正确的,但可能不是。 感谢 cmets,我将全员参与并向团队报告。我在发布查询应该包含 DateTime.Now.AddMonths(-11) 而不是来自 searchrequest 的代码时确实犯了一个错误。 在验证 DateTime 操作和与可为空的 Date 或 DateTime 属性的比较可能不是直接责任后,我已经更新了答案,尽管奇怪的是,删除该条件似乎允许查询跑步。我确实验证了这些操作确实适用于 EF Core 3.1 和 5。如果不解决某种形式的复杂性问题,但减少表面积以寻找潜在问题,该解决方案可能会涉及大幅简化查询。

以上是关于关于为啥这个 linq ef 核心查询不会执行的建议的主要内容,如果未能解决你的问题,请参考以下文章

为啥 LINQ 在我的查询中使用错误的数据类型,而它在 EF 架构中被正确声明?

Linq WHERE EF.Functions.Like - 为啥直接属性起作用而反射不起作用?

无法从 Linq 查询 EF Core 访问字段值

Linq to EF 系统不支持异常 System.NotSupportedException

如何通过一个数据库往返执行两个 EF linq 查询

linq和EF查询的用法和区分