Hibernate检索UTC中的时间戳



我使用hibernate+spring,希望以UTC存储/加载时间戳。我读到我应该添加一个属性,所以我把它添加到我的application.properties 中

spring.jpa.properties[hibernate.jdbc.time_zone]=UTC

这解决了问题的一部分——现在日期保存在数据库的utc中。但当我检索时间戳时,它们会转换为默认时区。如何在不将默认时区设置为UTC的情况下解决此问题?实体的属性的类型为LocalDateTime。我运行了代码,注意到在get(接受日历的方法)中使用了正确的结果集方法,该方法具有存储UTC的区域信息的实例。但是,在将日历的值设置为从数据库中检索到的值之后,日历将被转换为时间戳,代码为

Timestamp ts = new Timestamp(c.getTimeInMillis());

在调试模式中,我看到ts将时间戳值为的cdate字段存储在默认时区(而不是UTC)。

首先,如果我们谈论的是Hibernate 5(准确地说是5.2.3-5.6.x),hibernate.jdbc.time_zone设置的目的不是让应用程序开发人员能够实现某种复杂的日期/时间逻辑,而是让持久性提供方与底层数据库同步,这在相应的CR:中有明确的说明

当前我的数据库有UTC的隐含日期时间。没有分区数据被附加到字符串的末尾(例如"2013-10-14 04:00:00")。当Hibernate将其作为ZonedDateTime读取时,它错误地将其作为EST读取,因为EST是JVM的TimeZone。如果能够通过注释指定字段的时区,那就太好了。

基本上:如果(mine:且仅当)像SELECT SYSDATE FROM DUAL(SELECT LOCALTIMESTAMP用于PostgreSQL等)这样的SQL语句返回了一些你没有预料到的东西,你肯定需要设置hibernate.jdbc.time_zone,在这种情况下,Hibernate将开始将非时区感知JDBC数据调整为对应用程序来说或多或少可靠的数据-这正是您所观察到的(当我检索时间戳时,它们将转换为默认时区)

其次,关于JSR-310和JDBC 4.2的任何推测(比如对于时区感知的java类型,您需要将DB列定义为timestamp with time zone),在Hibernate 5的情况下都是不正确的,这也在相应的CR中提到:

;存储的TZ";实际上取决于数据库/驱动程序如何处理TIMESTAMP;TIMESTAMP_WITH_TIMEZONE";类型我个人认为将特定的TZ差异保存到DB是一个巨大的错误,因此我个人将继续不支持TIMESTAMP_WITH_TIMEZONE类型。这意味着我们永远不必绑定Calendar,因为我们可以简单地将值转换为JVM/JDBC TZ。具体来说,我建议我们(继续)假设驱动程序已经设置好,以便在。。。

事实上,如果你试图在Hibernate 5源中找到java.sql.Types#TIMESTAMP_WITH_TIMEZONE的用法,你会一无所获,只是因为当时Hibernate开发人员对不同Java版本、Java类型、,DB引擎和JDBC驱动程序(它们正在开发最流行的(mine:唯一一个)JPA实现,这肯定与开发微服务不同),然而,Hibernate 6中有很多相关的更改(例如,检查TimeZoneStorageType)。在Hibernate 5中,所有时区转换逻辑都通过TimestampTypeDescriptor:

@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return options.getJdbcTimeZone() != null ?
javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance( options.getJdbcTimeZone() ) ), options ) :
javaTypeDescriptor.wrap( rs.getTimestamp( name ), options );
}

正如您所看到的,Hibernate 5只是向JDBC驱动程序提供了一个提示,最后一个应该如何处理#getTimestamp调用:

检索JDBC TIMESTAMP参数的值作为java.sql.TIMESTAMP对象,使用给定的Calendar对象来构造TIMESTAMP对象。使用Calendar对象,驱动程序可以在考虑自定义时区和区域设置的情况下计算时间戳。如果未指定Calendar对象,则驱动程序将使用默认时区和区域设置。

关于您的案例:

您要么需要使用支持时区的java类型(ZonedDateTime/OffsetDateTime,甚至Instant),要么编写自己的Hibernate类型,它将处理时区转换——这并不像看上去那么难。

我们还可以设置`per-session base:

session = HibernateUtil.getSessionFactory().withOptions()
.jdbcTimeZone(TimeZone.getTimeZone("UTC"))
.openSession();

我的数据库时区是UTC,在我的应用程序时区中,我通过让实体和表都有UTC日期来解决这个问题,这样它们之间就不需要转换了。然后,我在getter和setter中的代码中进行时间戳之间的转换。然后我认为你可以手动完成。

该字段的Setter和getter

public void setCreatedDate(LocalDateTime createdAt)
{         
this.createdAt = createdAt.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
}
public LocalDateTime getCreatedDate()
{         
return createdAt.atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();
}

正如Andrey在回答中提到的那样,在Hibernate 6中,将日期/时间标准化为UTC的方法是使用java.time类型OffsetDateTimeZonedDateTime,并且:

  • 注释字段@TimeZoneStorage(NORMALIZE_UTC),或
  • 设置属性CCD_ 16

我不太确定你在这里使用LocalDateTime是什么意思。将本地日期时间标准化为UTC意味着什么?这句话真的没有意义:你不能将本地日期时间移动到新的时区,因为它一开始没有关联的时区。

我认为你的意思是你的";本地";日期时间实际上是当前JVM时区中的分区日期时间。但如果是这样的话,使用localDateTime.atZone(ZoneId.systemDefault())来正确地表示这种情况是非常容易的。

只需使用Instant(Hibernate理解这一点)。在您的实体类中,它就这么简单

@Column(name="time_when_something_happened", columnDefinition="TIMESTAMP")
private Instant timeWhenSomethingHappened;

仅此而已。无需设置spring.jpa.properties.hibernate.jdbc.time_zone=UTC属性,因为Hibernate知道这是隐含的,因为您使用的是Instant

使用Hibernate 6.2、Spring Boot 3.1和PostgreSQL 11进行了测试。

以下是您应该(大概)使用Instant的原因:对于大多数应用程序,要求是记录发生的事情的时间戳。注:过去时。因此,这将是Java中的Instant类型,而不是LocalDateTimeOffsetDateTime

您的应用程序可能与后端有关?。持久性是从最终用户的角度来看的一个实现细节。换句话说:例如使用OffsetDateTime真的没有意义,因为携带这样的信息作为偏移是没有意义的。只需推迟决定您希望如何显示时间戳值,直到您真正面对一个人类用户,如在UI中。一句话:它是Instant你想要在你的持久层!(*)

*)也许有几句话可以解释为什么你会看到这么多so和Blog的答案,引导你使用java类型的LocalDateTimeOffsetDateTime,而真正需要的确实是记录事情发生的时间:这是因为JPA标准(直到最近)都没有提到Instant。很长一段时间以来,使用Instant而不需要像我建议的那样做任何事情是可能的,但它一直是Hibernate的专长。这个问题(我称之为遗漏)早在2017年就被记录在JPA规范中,但直到现在,也就是2023年8月,才看到一些行动。因此,它确实将成为JPA规范的一部分,也许是在v3.2中。考虑到这一点,我肯定认为您应该使用Instant:它很简单,而且它传达了意图。

最新更新