实体框架核心 NodaTime 总和持续时间

Posted

技术标签:

【中文标题】实体框架核心 NodaTime 总和持续时间【英文标题】:Entity Framework Core NodaTime Sum Duration 【发布时间】:2021-09-30 16:18:22 【问题描述】:

EF Core中下面的sql怎么写

select r."Date", sum(r."DurationActual")
from public."Reports" r
group by r."Date"

我们有以下模型 (mwe)

public class Report 

    public LocalDate Date  get; set; 
    public Duration DurationActual  get; set;   ​

我尝试了以下方法:

await dbContext.Reports
    .GroupBy(r => r.Date)
    .Select(g => new
    
      g.Key,
      SummedDurationActual = g.Sum(r => r.DurationActual),
    )
    .ToListAsync(cancellationToken);

但这不会编译,因为Sum 仅适用于intdoublefloatNullable<int> 等。

我还尝试总结总小时数

await dbContext.Reports
    .GroupBy(r => r.Date)
    .Select(g => new
    
      g.Key,
      SummedDurationActual = g.Sum(r => r.DurationActual.TotalHours),
    )
    .ToListAsync(cancellationToken)

可以编译但不能被 EF 翻译并出现以下错误

System.InvalidOperationException: The LINQ expression 'GroupByShaperExpression:
KeySelector: r.Date, 
ElementSelector:EntityShaperExpression: 
    EntityType: Report
    ValueBufferExpression: 
        ProjectionBindingExpression: EmptyProjectionMember
    IsNullable: False

    .Sum(r => r.DurationActual.TotalHours)' 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 'AsEnumerable', ....

当然我可以早点列举出来,但这效率不高。

进一步澄清一下:我们使用Npgsql.EntityFrameworkCore.PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQL.NodaTime 来建立连接。 Duration 是来自 NodaTime 的 DataType,表示类似于 TimeSpan 的东西。 Duration 被映射到数据库端的 interval

我们大量使用使用 InMemoryDatabase (UseInMemoryDatabase) 的单元测试,因此该解决方案应该适用于 PsQl 和 InMemory。

对于那些不熟悉 NodaTime 的 EF-Core 集成的人:

你在配置中添加UseNodaTime()方法调用,例子:

services.AddDbContext<AppIdentityDbContext>(
    options => options
                      .UseLazyLoadingProxies()
                      .UseNpgsql(configuration.GetConnectionString("DbConnection"),
                            o => o
                                 .MigrationsAssembly(Assembly.GetAssembly(typeof(DependencyInjection))!.FullName)
                                 .UseNodaTime()
                        )

这为 NodaTime 类型添加了类型映射

.AddMapping(new NpgsqlTypeMappingBuilder
                
                    PgTypeName = "interval",
                    NpgsqlDbType = NpgsqlDbType.Interval,
                    ClrTypes = new[]  typeof(Period), typeof(Duration), typeof(TimeSpan), typeof(NpgsqlTimeSpan) ,
                    TypeHandlerFactory = new IntervalHandlerFactory()
                .Build()

我不知道每个细节,但我认为这增加了一个 ValueConverter。 更多信息:https://www.npgsql.org/efcore/mapping/nodatime.html

【问题讨论】:

数据库中DurationActual 列的实际类型是什么?你的模型需要与之匹配。然后,您可以拥有一个未映射的属性,将原始类型转换为 Duration 并返回。 实际类型为interval。它由 ef 在Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime 的帮助下自动映射我们可以在 C# 中使用 Duration 没问题,一切正常。我关心的是在数据库中总结它们。 您是否在DurationActual 上配置了任何值转换器? 不,我没有直接。 Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime 应该加一个。我已经更新了问题以进一步解释 您会对原始 SQL 实现不满意吗?鉴于 NodaTime 高度特定于 postgres 并且您使用的是非标准类型(Duration 而不是 Timespan),如果不为此数据类型编写自己的 linq 表达式处理程序或要求 NodaTime 开发人员这样做,则没有其他方法可以优化它. 【参考方案1】:

查看Npgsql.EntityFrameworkCore.PostgreSQL的源代码here,可以看到不能翻译Duration的成员。 如果使用的成员属于类型不是LocalDateTimeLocalDateLocalTimePeriod 的对象,则方法Translate 将返回null。在您的情况下,使用的成员是 TotalHours 并且它属于类型为 Duration 的对象。

因此,如果您将 DurationActual 的类型从 Duration 更改为 PeriodTimeSpan 也可以),那么您的第二个示例就可以工作。

不过,Period 的成员 TotalHours 也无法翻译(请参阅 code here 了解可翻译成员的完整列表)。

所以你必须自己计算这个值:

 await dbContext.Reports
                .GroupBy(r => r.Date)
                .Select(g => new
                
                    g.Key,
                    SummedDurationActual = g.Sum(r => r.DurationActual.Hours + ((double)r.DurationActual.Minutes / 60) + ((double)r.DurationActual.Seconds / 3600)),
                )
                .ToListAsync(cancellationToken)

如果更改 DurationActual 的类型不是一个选项,您可以向 Npgsql 开发人员提出问题以添加必要的翻译。他们建议在their documentation这样做:

请注意,该插件远未涵盖所有翻译。如果缺少您需要的翻译,请打开一个问题来请求它。

【讨论】:

谢谢!您的回答有助于引导正确的方向。我添加了一个拉取请求 (github.com/npgsql/efcore.pg/pull/1931) 以添加对 TotalHours 的支持。我还实现了一个自定义扩展来解决我现在的问题。 很高兴能帮上忙!感谢您创建 PR 并分享您的解决方案。 fyi,如果您需要它,我更新了 pullrequest 以包括(几乎)所有 Duration 成员,它现在已合并 -> 在下一个版本中您可以使用它。 谢谢,我会在下一个版本发布时尝试更新我的答案。【参考方案2】:

@mohamed-amazirh 的回答帮助我朝着正确的方向前进。 无法更改为 Period(因为它根本不是句点,而是持续时间)。 我最终写了一个IDbContextOptionsExtension 来满足我的需求。

完整代码在这里:

public class NpgsqlNodaTimeDurationOptionsExtension : IDbContextOptionsExtension

    private class ExtInfo : DbContextOptionsExtensionInfo
    
        public ExtInfo(IDbContextOptionsExtension extension) : base(extension)  

        public override long GetServiceProviderHashCode()
        
            return 0;
        

        public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
        
            return;
        

        public override bool IsDatabaseProvider => false;
        public override string LogFragment => "using NodaTimeDurationExt ";
    

    public NpgsqlNodaTimeDurationOptionsExtension()
    
        Info = new ExtInfo(this);
    

    public void ApplyServices(IServiceCollection services)
    
        new EntityFrameworkRelationalServicesBuilder(services)
            .TryAddProviderSpecificServices(x => x.TryAddSingletonEnumerable<IMemberTranslatorPlugin, NpgsqlNodaTimeDurationMemberTranslatorPlugin>());
    

    public void Validate(IDbContextOptions options)  

    public DbContextOptionsExtensionInfo Info  get; set; 


public class NpgsqlNodaTimeDurationMemberTranslatorPlugin: IMemberTranslatorPlugin

    public NpgsqlNodaTimeDurationMemberTranslatorPlugin(ISqlExpressionFactory sqlExpressionFactory)
    
        Translators = new IMemberTranslator[]
        
            new NpgsqlNodaTimeDurationMemberTranslator(sqlExpressionFactory),
        ;
    

    public IEnumerable<IMemberTranslator> Translators  get; set; 


public class NpgsqlNodaTimeDurationMemberTranslator : IMemberTranslator

    private readonly ISqlExpressionFactory sqlExpressionFactory;

    public NpgsqlNodaTimeDurationMemberTranslator(ISqlExpressionFactory sqlExpressionFactory)
    
        this.sqlExpressionFactory = sqlExpressionFactory;
    

    public SqlExpression Translate(SqlExpression instance, MemberInfo member, Type returnType, IDiagnosticsLogger<DbLoggerCategory.Query> logger)
    
        var declaringType = member.DeclaringType;
        if (instance is not null
            && declaringType == typeof(Duration))
        
            return TranslateDuration(instance, member, returnType);
        

        return null;
    

    private SqlExpression? TranslateDuration(SqlExpression instance, MemberInfo member, Type returnType)
    
        return member.Name switch
        
            nameof(Duration.TotalHours) => sqlExpressionFactory
                .Divide(sqlExpressionFactory
                        .Function("DATE_PART",
                            new[]
                            
                                sqlExpressionFactory.Constant("EPOCH"),
                                instance,
                            ,
                            true, new[]  true, true ,
                            typeof(double)
                        ),
                    sqlExpressionFactory.Constant(3600)
                ),
            _ => null,
        ;
    

要使用它,我必须像 NodaTime 一样添加它:

services.AddDbContext<Cockpit2DbContext>(options =>
    
        options
            .UseLazyLoadingProxies()
            .UseNpgsql(configuration.GetConnectionString("Cockpit2DbContext"),
                o =>
                
                    o.UseNodaTime();
                    var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure) o).OptionsBuilder;
                    var ext = coreOptionsBuilder.Options.FindExtension<NpgsqlNodaTimeDurationOptionsExtension>() ?? new NpgsqlNodaTimeDurationOptionsExtension();
                    ((IDbContextOptionsBuilderInfrastructure) coreOptionsBuilder).AddOrUpdateExtension(ext);
                )
            .ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning));
    
);
            

【讨论】:

以上是关于实体框架核心 NodaTime 总和持续时间的主要内容,如果未能解决你的问题,请参考以下文章

如何在条件下使用实体框架组日期而不是时间日期和总和

使用 SQL Azure 从会话状态提供程序(实体框架)持续接收“超时时间已过”

按摩框不显示实体框架核心[重复]

为啥实体框架核心加载实体的关系而不添加包含

如何指定实体框架核心表映射?

使用实体框架核心生成和访问存储过程