环境
- Spring Boot Starter数据JPA 1.4.2
- Eclipselink 2.5.0
- Postgresql 9.4.1211.jre7
问题
我正在构建一个SpringBoot微服务,它与另一个服务共享Postgresql数据库。数据库从外部初始化(超出我们的控制范围),其他服务使用的日期时间列类型为不带时区的时间戳。因此,由于我希望数据库中的所有日期都具有相同的类型,因此对于我的JPA实体日期来说,具有该类型是必需的。
我在JPA实体对象上映射它们的方式如下:
@Column(name = "some_date", nullable = false)
private Timestamp someDate;
问题是,当我创建一个时间戳如下:
new java.sql.Timestamp(System.currentTimeMillis())
我查看数据库,时间戳包含我的本地时区日期时间,但我想将其存储在UTC中。这是因为我的默认时区设置为"欧洲/布鲁塞尔",JPA/JDBC在将java.sql.Timestamp
对象放入数据库之前将其转换为时区。
发现不理想的解决方案
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
具有我想要实现的效果,但它不适合,因为它不是针对我的服务的。也就是说,它将影响整个JVM或当前线程加上子线程。用
-Duser.timezone=GMT
启动应用程序似乎也可以为正在运行的JVM的单个实例完成这项工作。因此,这是一个比上一个更好的解决方案。
但是有没有办法在JPA/datasource/spring引导配置中指定时区?
我能想出的解决这个问题的最合适的方法是使用AttributeConverter
将Java 8ZonedDateTime
对象转换为java.sql.Timestamp
对象,这样它们就可以映射到PostgreSQLtimestamp 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
对象。这样,无论我的应用程序运行在哪个时区,我都会保留数据库上可以看到的确切日期时间。
由于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
版本才能使序列化工作。在这个答案中有更多关于这一点的内容。
你不能。Eclipselink使用setTimestamp
的单个arg版本,将时区处理的责任委派给驱动程序,而postgresqljdbc驱动程序不允许覆盖默认时区。postgres驱动程序甚至将客户端时区传播到会话,因此服务器端默认值对您也没有用处。
你可以尝试解决一些棘手的问题,例如写一个JPA 2.1AttributeConverter
来将你的时间戳转移到目的地区域,但最终它们注定要失败,因为你的客户时区有夏令时调整,使某些时间变得模糊或不可代表。
您必须在客户端上设置默认时区,或者使用本机SQL将时间戳设置为带强制转换的字符串。
使用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")));
}
}