簡體   English   中英

Entity Framework Core NodaTime 總和持續時間

[英]Entity Framework Core NodaTime Sum Duration

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

這里查看Npgsql.EntityFrameworkCore.PostgreSQL的源代碼,您可以看到它無法翻譯Duration的成員。 如果使用的成員屬於類型不是LocalDateTimeLocalDateLocalTimePeriod的對象,則Translate方法將返回null 在您的情況下,使用的成員是TotalHours並且它屬於類型為Duration的對象。

因此,如果您將DurationActual的類型從Duration更改為PeriodTimeSpan也可以使用),則可以使您的第二個示例起作用。

盡管如此, Period的成員TotalHours也無法翻譯(有關可以翻譯的成員的完整列表,請參閱此處代碼)。

所以你必須自己計算這個值,如下所示:

 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 開發人員提出問題以添加必要的翻譯。 他們建議在他們的文檔中這樣做:

請注意,該插件遠未涵蓋所有翻譯。 如果缺少您需要的翻譯,請打開一個問題以請求它。

@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));
    }
);
            

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM