【问题标题】:Entity Framework Core NodaTime Sum Duration实体框架核心 NodaTime 总和持续时间
【发布时间】: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 开发人员这样做,则没有其他方法可以优化它.

标签: c# postgresql entity-framework-core 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));
    }
);
            

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-05-10
    • 2021-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-29
    • 1970-01-01
    • 2017-04-03
    相关资源
    最近更新 更多