我需要按其
CreatedAt
属性对交易进行分组。但在特定时区。
像这样
SELECT
SUM("Amount")
FROM
public."Transactions"
GROUP BY
CAST("CreatedAt" AS date) AT TIME ZONE 'America/Chicago'
但使用 EF Core。
我已经看到了 Mehdi 的这种方法,并尝试针对 PostgreSql 修改它。
这是我所拥有的 - 表达式生成器:
public class AtTimeZoneExpression : SqlFunctionExpression
{
private readonly IReadOnlyCollection<SqlExpression> _params;
public AtTimeZoneExpression(IReadOnlyCollection<SqlExpression> parameters)
: base("notimportant", true, typeof(DateTimeOffset), RelationalTypeMapping.NullMapping)
{
_params = parameters;
}
protected override Expression Accept(ExpressionVisitor visitor)
{
if (visitor is not QuerySqlGenerator sqlGenerator)
return base.Accept(visitor);
if (_params.First().TypeMapping.DbType == System.Data.DbType.DateTimeOffset)
visitor.Visit(new SqlFragmentExpression("CAST( "));
visitor.Visit(_params.First());
if (_params.First().TypeMapping.DbType == System.Data.DbType.DateTimeOffset)
visitor.Visit(new SqlFragmentExpression(" as date)"));
visitor.Visit(new SqlFragmentExpression(" AT TIME ZONE "));
visitor.Visit(_params.Skip(1).First());
return this;
}
protected override void Print([NotNull] ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append("AT TIME ZONE Expression");
}
}
功能:
public static class QueryHelper
{
public static DateTimeOffset ToTimeZone(this DateTimeOffset source, string timeZone)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
var date = TimeZoneInfo.ConvertTimeFromUtc(source.UtcDateTime, tz);
return new DateTimeOffset(date, tz.GetUtcOffset(date));
}
}
模型构建器:
modelBuilder.HasDbFunction(typeof(QueryHelper).GetMethod(nameof(QueryHelper.ToTimeZone), new[] { typeof(DateTimeOffset), typeof(string) }))
.HasTranslation(args => new AtTimeZoneExpression(args));
及用法:
var activityPerDay = await transactionQuery
.GroupBy(t => t.CreatedAt.ToTimeZone(timeZoneId).Date)
.Select(dayGroup => new ItemMetrics
{
DateTime = dayGroup.Key,
Volume = dayGroup.Sum(t => t.Amount ?? 0)
})
.ToListAsync(cancellationToken);
我使用的是 EF Core 版本 8.0.10。
这就是我得到的 t => t.CreatedAt.ToTimeZone(timeZoneId).Date
(
CreatedAt
是不可为空的 DateTimeOffset
列)
System.ArgumentException:AtTimeZone 的时间戳参数具有未知的存储类型 NULL(参数“时间戳”)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.NpgsqlSqlExpressionFactory.AtTimeZone(SqlExpression timestamp, SqlExpression timeZone, Type type, RelationalTypeMapping typeMapping)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.NpgsqlSqlExpressionFactory.AtUtc(SqlExpression timestamp, RelationalTypeMapping typeMapping)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal.NpgsqlDateTimeMemberTranslator.TranslateDateTimeOffset(SqlExpression instance, MemberInfo member)
at Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal.NpgsqlDateTimeMemberTranslator.Translate(SqlExpression instance, MemberInfo member, Type returnType, IDiagnosticsLogger`1 logger)
at Microsoft.EntityFrameworkCore.Query.RelationalMemberTranslatorProvider.<>c__DisplayClass6_0.<Translate>b__0(IMemberTranslator t)
at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
我还尝试将
NodaTime
添加到上下文并使用可翻译函数。
builder.Services.AddDbContext<MyContext>(options => options.UseNpgsql(
"<connection string>",
o => o.UseNodaTime()));
但这也不起作用。
根据日期和时间函数的文档以及我的测试,不需要额外的函数 -
TimeZoneInfo.ConvertTimeBySystemTimeZoneId
按预期工作。这是一个例子:
var activityPerDay = await transactionQuery
.GroupBy(t => TimeZoneInfo.ConvertTimeBySystemTimeZoneId(t.CreatedAt, timeZoneId).Date)
.Select(dayGroup => new ItemMetrics
{
DateTime = dayGroup.Key,
Volume = dayGroup.Sum(t => t.Amount ?? 0)
})
.ToListAsync(cancellationToken);