如何解决Entity Framework Core NodaTime 总和持续时间 对于那些不熟悉 NodaTime 的 EF-Core 集成的人:
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
的数据类型,表示类似于 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
解决方法
查看Npgsql.EntityFrameworkCore.PostgreSQL
的源代码here,可以看到它无法翻译Duration
的成员。
如果使用的成员属于类型不是 Translate
、null
、LocalDateTime
或 LocalDate
的对象,则方法 LocalTime
将返回 Period
。在您的情况下,使用的成员是 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 中这样做:
请注意,该插件远未涵盖所有翻译。如果缺少您需要的翻译,请打开一个问题来请求它。
,@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,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));
}
);
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。