如何为Spring Boot JPA Timestamp指定UTC时区

11

环境

  • Spring Boot Starter Data JPA 1.4.2
  • Eclipselink 2.5.0
  • Postgresql 9.4.1211.jre7

问题

我正在构建一个使用Postgresql数据库的Spring Boot微服务,并与另一个服务共享该数据库。该数据库在外部进行初始化(超出我们的控制范围),而其他服务使用的日期时间列类型为没有时区的时间戳(timestamp without time zone)。因此,由于我希望数据库中所有日期都具有相同类型,在我的JPA实体日期中使用该类型是必需的。

我将它们映射到我的JPA实体对象上的方式如下:

@Column(name = "some_date", nullable = false)
private Timestamp someDate;

问题在于,当我按照以下方式创建一个时间戳时:

new java.sql.Timestamp(System.currentTimeMillis())

我查看了数据库,时间戳包含了我的本地时区日期时间,但我想将其存储为UTC。这是因为我的默认时区设置为'Europe/Brussels',JPA/JDBC在将我的java.sql.Timestamp对象放入数据库之前会将其转换为我的时区。

找到并不理想的解决方法

  • TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC")); 可以达到我想要的效果,但它不适用于我的服务。即它将影响整个JVM或当前线程及其子线程。

  • 使用 -Duser.timezone=GMT 启动应用程序似乎也可以针对单个正在运行的JVM实例完成任务。因此,比上一个解决方法更好。

但是,是否可以通过JPA /数据源 / Spring Boot配置指定时区?


你如何“查看数据库”?也许这只是PostgreSQL将时间戳调整为您会话时区的结果?请参阅此答案 - Adam Michalik
感谢您回答@Adam,我只是SELECT * FROM my_table。根据您链接的答案,_"当您稍后显示该时间戳时,您会得到您字面输入的内容"_。如果我SELECT my_timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'UTC' FROM my_table,我看到相同的结果,这很有道理。考虑到这一点,我认为这不是PostgreSQL调整时区,调整似乎发生在Java端将值放入数据库之前。 - oizulain
3个回答

6
我找到的解决该问题最合适的方法是使用一个AttributeConverter将Java 8 ZonedDateTime对象转换为java.sql.Timestamp对象,这样它们就可以映射到PostgreSQL timestamp without time zone类型。您需要使用AttributeConverter的原因是因为Java 8/Joda时间日期类型与JPA不兼容AttributeConverter如下所示:
@Converter(autoApply = true)
public class ZonedDateTimeAttributeConverter implements AttributeConverter<ZonedDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(ZonedDateTime zonedDateTime) {
        return (zonedDateTime == null ? null : Timestamp.valueOf(zonedDateTime.toLocalDateTime()));
    }

    @Override
    public ZonedDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
        return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime().atZone(ZoneId.of("UTC")));
    }

}

这让我能够将没有时区信息的数据库时间戳读取为带有UTC时区的ZonedDateTime对象。这样,无论我的应用程序运行在哪个时区,我都可以保留可在db上看到的确切日期时间。
由于toLocalDateTime()也应用了系统的默认时区转换,因此这个AttributeConverter基本上取消了JDBC驱动程序所应用的转换。
你真的需要使用timestamp without timezone吗?
事实上,如果您将日期时间信息存储在时区中(即使它是UTC),则timestamp without timezone PostgreSQL类型是错误的选择。正确的数据类型应该是包含时区信息的timestamp with timezone。更多关于此话题的内容请参见此处
然而,如果出于任何原因,您必须使用timestamp without timezone,我认为上述ZonedDateTime方法是一种强大且一致的解决方案。

您是否还将ZonedDateTime序列化为JSON?

那么,您可能会对以下事实感兴趣:为了使序列化工作正常,您需要至少使用jackson-datatype-jsr310依赖的版本2.6.0。有关更多信息,请参见此答案


1
你做不到。Eclipselink使用setTimestamp的单参数版本,将时区处理的责任委托给驱动程序,而postgresql jdbc驱动程序不允许覆盖默认时区。甚至postgres驱动程序将客户端时区传播到会话中,因此服务器端默认值对您也没有用。

有一些hackish的方法可以尝试解决问题,例如编写JPA 2.1 AttributeConverter将您的时间戳转换为目标区域,但最终它们注定会失败,因为您的客户端时区具有夏令时调整,使得某些时间模糊或无法表示。

您需要在客户端上设置默认时区,或者使用本地SQL以字符串形式设置时间戳并进行强制转换。


0

使用Instant而不是LocalDateTime怎么样?我认为它可以将实体值转换为UTC时间戳

@Converter(autoApply = true)
public class ZonedDateTimeAttributeConverter implements         AttributeConverter<ZonedDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(ZonedDateTime zonedDateTime) {
            return (zonedDateTime == null ? null : Timestamp.from(zonedDateTime.toInstant()));
    }

    @Override
    public ZonedDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
        return (sqlTimestamp == null ? null : sqlTimestamp.toInstant().atZone(ZoneId.of("UTC")));
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接