Postgres UTC到Java ZonedDateTime

时间:2016-04-12 03:36:14

标签: java spring-data-jpa postgresql-9.3

对我原来issue的跟进问题。

因为我知道所使用的任何日期时间都是针对已知时区的,而不是用户可能提交请求的时间,我会采用LocalDateTime,转换为UTC并保留。然后,当检索到约会时,我将保存的时间转换为会议位置时区(存储在db中)。但是,似乎我保存的值实际上是保存在我当地的时区。

我在Rest Controller中收到日期时间值,例如:

startLocalDateTime: 2016-04-11T10:00
endLocalDateTime: 2016-04-11T10:30

约会有两个ZoneDateTime字段:

@Column(name = "startDateTime", columnDefinition= "TIMESTAMP WITH TIME ZONE")
private ZonedDateTime startDateTime;
@Column(name = "endDateTime", columnDefinition= "TIMESTAMP WITH TIME ZONE")
private ZonedDateTime endDateTime;

然后我将值更改为UTC并存储在我的实体上以存储到Postgres:

appointment.setStartDateTime(startLocalDateTime.atZone(ZoneId.of( "UTC" )))
appointment.setEndDateTime(endLocalDateTime.atZone(ZoneId.of( "UTC" )))

我将它存储在Postgres中(columnDefinition= "TIMESTAMP WITH TIME ZONE")当我查看pgadminIII中的记录时,我看到:

startDateTime "2016-04-11 04:00:00-06"
endDateTime  "2016-04-11 04:30:00-06"

所以这些似乎以UTC格式正确存储(如果到目前为止我做错了,请纠正我)。然后我从数据库中检索它们并将它们返回为:

Appointment
startdatetime: 2016-04-11T04:00-06:00[America/Denver]
enddatetime: 2016-04-11T04:30-06:00[America/Denver]

这些值以JSON:

的形式发回
{  
 "appointmentId":50,
 "startDateTime":"2016-04-11T04:00",
 "endDateTime":"2016-04-11T04:30"
}

所以即使我将它们保存为UTC,当我检索它们时,它们处于MST(我的本地)时区,而不是UTC,我无法将它们转换回实际时间。

仍然在坚持不懈。我已经尝试在我的实体上使用java.sql.timestamp,java.sql.Date,java.util.Date和java.time.ZonedDateTime。我的Postgres仍然是“带时区的时间戳”。但是因为我使用的是Spring-Data-JPA并且需要使用相同的类型进行查询。如果我使用Date - 应该是sql.Date还是util.Date?

2 个答案:

答案 0 :(得分:2)

您说:

  

我使用LocalDateTime,转换为UTC并坚持执行。

不。坚持使用LocalDateTime进行约会,不涉及UTC约会。

在预定约会应显示为某天的某个日期时,无论政治家如何重新定义其管辖时区中使用的偏移量,都应存储带有该日期的日期,但要保留该时区分离。

世界各地的政客们表现出一种奇怪的嗜好,他们经常重新定义其区域的偏移量,进行调整以匹配或区别其邻居,或者采用或放弃愚蠢的Daylight Saving Time (DST)。这些更改无需任何预警即可完成,甚至完全没有。因此,如果将来存储一天,现在看起来像是未来一天的2:30 PM,可能变成1:30 PM,2:00 PM,3:30 PM,或者谁知道政治家会提出什么建议,日期,时间,区域/偏移量)。

Java

仅表示日期和时间,请使用LocalDateTime。此类故意缺少时区或从UTC偏移的任何概念。这种缺乏意味着该课程没有 not 跟踪时刻,不是 not 在时间线上的一个点。因此,我们通常不在业务应用程序中使用此类。预订将来的约会是我们要做想要此类课程的少数情况之一。

LocalDate ld = LocalDate.of( 2020 , Month.JANUARY , 23 ) ;    // 2020-01-23. 
LocalTime lt = LocalTime.of( 14 , 30 ) ;                      // 2:30 PM.
LocalDateTime ldt = LocalDateTime.of( ld , lt ) ;

请参阅此code run live at IdeOne.com

  

ldt.toString():2020-01-23T14:30

使用JDBC 4.2和更高版本将其存储在数据库中。

myPreparedStatement.setObject( … , ldt ) ;

从数据库检索。

LocalDateTime ldt = myResultSet.getObject( … , LocalDateTime.class ); 

我们需要跟踪预期的时区。这是魁北克牙医诊所的约会吗?也代表这一事实。请记住,时区是特定区域的人们过去,现在和将来(计划的)UTC偏移量更改的历史记录。

使用ZoneId类表示时区。

ZoneId z = ZoneId.of( "America/Montreal" ) ;

将该区域名称保存为文本到数据库。通过调用ZoneId::toString获得其标识名称。 不要使用ZoneId::getDisplayName,因为这是为了生成本地化文本以显示给用户,但不是该区域的正式标识符。

myPreparedStatement.setString( … , z.toString() ) ;

从数据库中检索时,请重新构造ZoneId

String zoneIdString = myResultSet.getString( … ) ;
ZoneId z = ZoneId.of( zoneIdString ) ;

在为这些约会生成日历时,如果需要(临时)确定时间轴上的点,请将日期和时间与时区结合在一起。将ZoneId应用于LocalDateTime会产生ZonedDateTime。与ZonedDateTime不同,这LocalDateTime是时刻,在时间线上的一点。

 ZonedDateTime zdt = ldt.atZone( z ) ;

请参阅此code run live at IdeOne.com

  

zdt.toString():2020-01-23T14:30-05:00 [美国/蒙特利尔]

当然,我们存储该ZonedDateTime 。如以上所讨论的,如果那些忙碌的政客在现在和之后之间更改时区规则,那么现在看来23日下午2点30分将变成1:30 PM或3:30 PM。我们暂时仅临时使用此ZonedDateTime

数据库

我们仅将LocalDateTime存储在类型为TIMESTAMP WITHOUT TIME ZONE的列中。您错误地使用了WITH而不是WITHOUT,这是一个主要问题。对于WITHOUT类型,Postgres存储输入中给定的日期和时间。即使输入中包含区域或偏移量,Postgres也会忽略该区域或偏移量。

请注意,TIMESTAMP WITH TIME ZONE在Postgres中正好相反。输入随附的时区或偏移量信息用于将日期和时间调整为UTC。然后存储该UTC值。因此,此类型的列中的值都是全部采用UTC 。不幸的是,许多工具都具有在传递给您之前将默认时区动态应用于检索到的值的功能。当值实际上以UTC表示时,这会造成存储时区的错误幻想。检索为OffsetDateTime会告诉您真相。我之所以说OffsetDateTime是因为奇怪的是,JDBC 4.2规范要求支持该类,而不需要更常用的InstantZonedDateTime类。 JDBC驱动程序可以选择支持其他两个类。但是此段的所有内容都不存在,因为我们不是使用WITH列进行将来的约会。

如上所述,对于将来的约会,应将其存储在三(3)个单独的列中:

  • TIMESTAMP WITHOUT TIME ZONE表示日期和时间。
  • TEXT(或类似类型)作为我们要查看该约会的预期时区的标识名。
  • TEXT(或类似名称)在约会期间,采用标准ISO 8601格式。 (请参见下文)

问题要点

您说:

  

我在Rest Controller中收到一个日期时间值,例如:

     

startLocalDateTime:2016-04-11T10:00

因此解析为LocalDateTime。可以通过 java.time 类直接解析符合ISO 8601的字符串。

LocalDateTime ldt = LocalDateTime.parse( "2016-04-11T10:00" ) ;

您说:

  

endLocalDateTime:2016-04-11T10:30

不。不要存储结束时间。如果此时发生时区异常,您现在期望的结束时间可能会有所不同。例如,如果您的政客采用夏令时(DST),那么在此任命期间发生了DST的“春季提前”一小时更改?然后,您的半小时会议应该从10:00 AM到11:30 AM进行,而您录制的结束时间会错误地读取“ 10:30 AM”结尾。

通常,处理约会的最佳方法是记录约会的持续时间,而不是按时间记录。如上所述,约会由三部分数据组成:(1)带有日期的开始日期;(2)预期的时区;以及(3)约会的持续时间。应该根据需要使用fresh current time zone data动态计算结尾,以供临时使用。

用于记录此类持续时间的ISO 8601 standard includes a textual formatPnYnMnDTnHnMnS,其中P标记开始,而T将任何年月日与任何时分秒分开。 java.time PeriodDuration可以解析此类字符串。

Duration d = Duration.ofHours( 1 ) ;
String output = d.toString() ;

请参阅此代码在IdeOne.com上实时运行。

  

输出:PT1H

您可以将此持续时间动态应用于LocalDateTime

LocalDateTime ending = ldt.plus( d ) ;

同上ZonedDateTime

ZonedDateTime ending = zdt.plus( d ) ;

您说:

  

约会有两个ZoneDateTime字段:

不。将该字段设为LocalDateTime,而不是ZonedDateTime

您说:

  

@Column(name =“ startDateTime”,columnDefinition =“与时区的时间戳”)

不。将“ TIMESTAMP WITHTIME TIME ZONE”类型的列设置为可跟踪日期和时间,而无需使用区域/偏移量。

您说:

  

然后我将值更改为UTC并存储在我的实体上以存储到Postgres:

不。预订约会是我们不希望使用UTC的少数情况之一。在跟踪时刻时,我们通常希望使用UTC。但是未来的约会不是片刻。我们不知道现在的时刻,直到我们将时区应用于日期中的日期,并且我们无法提前做到这一点,因为我们不能信任政客。

您说:

  

仍然在坚持中挣扎。

在这里无法帮助您。我不使用Spring或JPA,因为它们可以解决我没有的问题。我使用直接JDBC。

您说:

  

因此,这些文件似乎已以UTC格式正确存储(如果到目前为止我做错了什么,请纠正我)。

不,不使用UTC,不存储在UTC中,不用于约会。

不要暂时使用UTC。例如,您的日志都应该在UTC发生事件的每个时刻进行报告。程序员和系统管理员最好在工作中学习如何在UTC中思考。将您桌上的第二个时钟设置为UTC。

您说:

  

当我检索它们时,它们位于MST(我的本地)时区,而不是UTC,并且我无法将它们转换回实际时间。

在我们的预约中,我们没有使用UTC,也没有使用任何其他时区或UTC偏移时间。因此,您的问题就化为乌有。

提示:正如我在上面所述,您的工具可能会骗您时区。如果需要,将工具和会话设置为UTC,以消除这种反功能。

您说:

  

我尝试在我的实体上使用java.sql.timestamp,java.sql.Date,java.util.Date和java.time.ZonedDateTime。

  • 请勿使用java.sql.Timestamp。替换为OffsetDateTime(和Instant)。
  • 请勿使用java.sql.Date。替换为LocalDate
  • 请勿使用java.util.Date。替换为Instant

仅使用几年前的现代 java.time 类取代了那些与最早的Java版本捆绑在一起的可怕的日期时间类。自JSR 310以来,那些糟糕的类就成为了遗产。

Table of date-time types in Java (both legacy and modern) and in standard SQL.

答案 1 :(得分:1)

jdbc驱动程序对你当前所在的时区有一些了解。一般来说,我已经通过让数据库为我做时区转换来解决这个问题,一些衍生的“时间戳没有时区AT TIME ZONE区”或“时区带有时区的时间戳'UTC'”。它是postgres jdbc驱动程序的核心,它正在计算JVM所处的时区并在保存中使用它。