"Entityframework Core 3 的 Linq 表达式无法被翻译。"

8

我刚刚升级到 EF 3,但是我之前能够正常工作的查询现在会抛出异常。

   ProductionRecords = _context.ProductionRecords
          .Where(r => r.DataCriacao.Date == DateTime.Now.Date)
            .Select(pr => new ProductionRecordViewModel
            {
                Id = pr.Id,
                Operador = pr.Operador,
                DataCriacao = pr.DataCriacao,
                Celula = pr.Celula.Name,
                Turno = pr.Turno.Name,
                TotalPecasSemDefeito = pr.ReferenceRecords.Sum(c => c.Quantity),
                TotalPecasComDefeito = pr.DefectRecords.Sum(c => c.Quantidade),
                TotalTempoParado = pr.StopRecords.Sum(c => Convert.ToInt32(c.Duration.TotalMinutes)),
            })
          .AsNoTracking()
          .ToList();

当我尝试用时间间隔和持续时间来计算集合总和时,会发生异常...

现在我该如何处理?

以下是异常信息:

InvalidOperationException: 无法翻译 LINQ 表达式 '(EntityShaperExpression: EntityType: StopRecord ValueBufferExpression: (ProjectionBindingExpression: EmptyProjectionMember) IsNullable: False ).Duration.TotalMinutes'。请以可翻译的形式重写查询,或通过显式插入 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync() 调用来切换到客户端评估。有关更多信息,请参见 https://go.microsoft.com/fwlink/?linkid=2101038


1
你能否更新问题,包括异常信息? - devNull
1
任何 .net 方法或类都无法转换为查询。你的 convert.toint() 和 Duration.totalminutes 可能是问题所在。如果你仍然想要它们存在,请在 select 前添加 .tolist()。 - aweyeahdawg
1
TotalTempoParado = pr.StopRecords pr.StopRecords.Sum(c => Convert.ToInt32(c.Duration.TotalMinutes)) 这是一个打字错误吗? - Erik Philips
1
你应该将这个问题发布到EF Core的GitHub上,以了解他们处理此类情况的愿景。 显然,如建议所述切换到客户端评估并不直接可行,并且添加急切/延迟加载会使其效率非常低下。 - Ivan Stoev
1
@ivanstoev 客户端评估是可以实现的,如果你调用 asnotracking 方法,然后调用 tolist 方法,最后对这些结果进行筛选。 - aweyeahdawg
显示剩余2条评论
2个回答

8

EF3 发生了一个重大改变,现在只有在查询链的最后才会自动回到客户端评估(你可能依赖于 Convert.ToInt32(c.Duration.TotalMinutes))。

尝试按以下方式重写查询:

 ProductionRecords = _context.ProductionRecords
      .Where(r => r.DataCriacao.Date == DateTime.Now.Date)
        .AsNoTracking()
        .AsEnumerable()
        .Select(pr => new ProductionRecordViewModel
        {
            Id = pr.Id,
            Operador = pr.Operador,
            DataCriacao = pr.DataCriacao,
            Celula = pr.Celula.Name,
            Turno = pr.Turno.Name,
            TotalPecasSemDefeito = pr.ReferenceRecords.Sum(c => c.Quantity),
            TotalPecasComDefeito = pr.DefectRecords.Sum(c => c.Quantidade),
            TotalTempoParado = pr.StopRecords pr.StopRecords.Sum(c => Convert.ToInt32(c.Duration.TotalMinutes)),
        })
      .ToList();
UPD 作为评论中恰当指出的 - 这将基本上延迟 .Select 的求值到客户端。这可能会导致性能问题。最可能,这种行为才是 EF Core 3 首次进行更改的原因。
我没有足够的具体信息来为您推荐适当的解决方案,但似乎您不能真正避免在所有结果上加载 StopRecords。编写自定义方法翻译器可以帮助您。请参见我的其他答案关于如何做到这一点。我快速检查了 EF Core 3 源代码,似乎 IMethodCallTranslator仍然存在。这意味着您有很高的机会构建一个自定义函数,将日期转换为 SQL 中的 TotalMinutes。

1
请注意,在使用 AsEnumerable 之前,必须先使用 AsNoTracking。 - Jackal
2
值得注意的是,这可能会导致对在Select子句中被汇总的三个表中的每个表进行额外的查询。潜在地会有N+1次查询。 - devNull
@devNull 我知道,但在我优化时间跨度之前,这样做就可以了。 - Jackal
1
@devNull 或者在未启用延迟加载的情况下,是 NRE 或 0 总和。 "答案" 基本上重复了异常消息和提出的客户端评估解决方案,这在一般情况下并不好。 - Ivan Stoev
1
要小心使用IEnumerable数据处理,因为它在应用程序级别进行处理,而且资源成本很高。 - AbuDawood
2
@IvanStoev 说得好,我增加了更多的上下文和一个解决根本原因的建议方式。希望这能为OP和社区增加足够的价值。 - timur

6

不要试图教导EF Core如何对时间跨度求和,不妨向数据库中添加一个计算列。

        public TimeSpan Duration { get; set; }
        public int Minutes { get; }


        entity.Property(e => e.Minutes)
            .HasComputedColumnSql("DATEDIFF(MINUTE, 0, Duration)");

我喜欢这种方法,我可以保留我的查询并只使用计算列,这样更有效率。我不会将其作为正确的问题,因为我已经接受了。 - Jackal

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