实体框架核心 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
仅适用于int
、double
、float
、Nullable<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.PostgreSQL
和Npgsql.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
的成员。
如果使用的成员属于类型不是LocalDateTime
、LocalDate
、LocalTime
或Period
的对象,则方法Translate
将返回null
。在您的情况下,使用的成员是 TotalHours
并且它属于类型为 Duration
的对象。
因此,如果您将 DurationActual
的类型从 Duration
更改为 Period
(TimeSpan
也可以),那么您的第二个示例就可以工作。
不过,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 总和持续时间的主要内容,如果未能解决你的问题,请参考以下文章