如何配置JPA/Hibernate将数据库中的日期/时间存储为UTC(GMT)时区?考虑这个带注释的JPA实体:
public class Event { @Id public int id; @Temporal(TemporalType.TIMESTAMP) public java.util.Date date; }
如果日期是太平洋标准时间(PST)的2008年2月3日上午9:30,那么我希望将数据库中存储的2008年2月3日下午5:30的UTC时间.同样,当从数据库中检索日期时,我希望它被解释为UTC.所以在这种情况下530pm是在UTC时间下午5点.当它显示时,它将被格式化为太平洋标准时间上午9:30.
使用Hibernate 5.2,您现在可以使用以下配置属性强制使用UTC时区:
有关更多详细信息,请查看此文章.
据我所知,您需要将整个Java应用程序放在UTC时区(以便Hibernate以UTC格式存储日期),并且您需要在显示内容时转换为所需的任何时区(至少我们这样做)这条路).
在启动时,我们做:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
并将所需的时区设置为DateFormat:
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Hibernate对Dates中的时区内容一无所知(因为没有),但它实际上是导致问题的JDBC层.ResultSet.getTimestamp
并且PreparedStatement.setTimestamp
两者都在他们的文档中说,默认情况下,当从/向数据库读取和写入时,它们会将日期转换为当前JVM时区.
我通过子类化在Hibernate 3.5中提出了一个解决方案org.hibernate.type.TimestampType
,强制这些JDBC方法使用UTC而不是本地时区:
public class UtcTimestampType extends TimestampType { private static final long serialVersionUID = 8088663383676984635L; private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); @Override public Object get(ResultSet rs, String name) throws SQLException { return rs.getTimestamp(name, Calendar.getInstance(UTC)); } @Override public void set(PreparedStatement st, Object value, int index) throws SQLException { Timestamp ts; if(value instanceof Timestamp) { ts = (Timestamp) value; } else { ts = new Timestamp(((java.util.Date) value).getTime()); } st.setTimestamp(index, ts, Calendar.getInstance(UTC)); } }
如果使用这些类型,则应该修复TimeType和DateType.缺点是您必须手动指定要使用这些类型而不是POJO中每个Date字段的默认值(并且还要打破纯JPA兼容性),除非有人知道更通用的覆盖方法.
更新:Hibernate 3.6已经更改了API类型.在3.6中,我写了一个类UtcTimestampTypeDescriptor来实现它.
public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); publicValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder ( javaTypeDescriptor, this ) { @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) ); } }; } public ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicExtractor ( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options ); } }; } }
现在,当应用程序启动时,如果将TimestampTypeDescriptor.INSTANCE设置为UtcTimestampTypeDescriptor的实例,则所有时间戳将被存储并视为以UTC格式,而不必更改POJO上的注释.[我还没有测试过这个]
通过Shaun Stone的提示添加一个完全基于并对于divestoclimb负债的答案.只是想详细说明,因为这是一个常见的问题,解决方案有点令人困惑.
这是使用Hibernate 4.1.4.Final,虽然我怀疑3.6之后的任何东西都可以工作.
首先,创建divestoclimb的UtcTimestampTypeDescriptor
public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); publicValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder ( javaTypeDescriptor, this ) { @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { st.setTimestamp( index, javaTypeDescriptor.unwrap( value, Timestamp.class, options ), Calendar.getInstance(UTC) ); } }; } public ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicExtractor ( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options ); } }; } }
然后创建UtcTimestampType,它在超级构造函数调用中使用UtcTimestampTypeDescriptor而不是TimestampTypeDescriptor作为SqlTypeDescriptor,否则将所有内容委托给TimestampType:
public class UtcTimestampType extends AbstractSingleColumnStandardBasicTypeimplements VersionType , LiteralType { public static final UtcTimestampType INSTANCE = new UtcTimestampType(); public UtcTimestampType() { super( UtcTimestampTypeDescriptor.INSTANCE, JdbcTimestampTypeDescriptor.INSTANCE ); } public String getName() { return TimestampType.INSTANCE.getName(); } @Override public String[] getRegistrationKeys() { return TimestampType.INSTANCE.getRegistrationKeys(); } public Date next(Date current, SessionImplementor session) { return TimestampType.INSTANCE.next(current, session); } public Date seed(SessionImplementor session) { return TimestampType.INSTANCE.seed(session); } public Comparator getComparator() { return TimestampType.INSTANCE.getComparator(); } public String objectToSQLString(Date value, Dialect dialect) throws Exception { return TimestampType.INSTANCE.objectToSQLString(value, dialect); } public Date fromStringValue(String xml) throws HibernateException { return TimestampType.INSTANCE.fromStringValue(xml); } }
最后,在初始化Hibernate配置时,将UtcTimestampType注册为类型覆盖:
configuration.registerTypeOverride(new UtcTimestampType());
现在,时间戳不应该关注JVM进出数据库的时区.HTH.
您会认为这个常见问题会由Hibernate处理.但它不!为了做到这一点,有一些"黑客".
我使用的是将Date作为Long存储在数据库中.所以我总是在1/1/70之后的几毫秒工作.然后,我的班级上有getter和setter,只返回/接受日期.因此API保持不变.不好的一面是我在数据库中长期存在.使用SQL我几乎只能做<,>,=比较 - 而不是花哨的日期运算符.
另一种方法是使用如下所述的自定义映射类型:http: //www.hibernate.org/100.html
我认为解决这个问题的正确方法是使用日历而不是日期.使用日历,您可以在持久化之前设置TimeZone.
注意:愚蠢的stackoverflow不会让我评论,所以这是对大卫的回应a.
如果您在芝加哥创建此对象:
new Date(0);
Hibernate将其保留为"12/31/1969 18:00:00".日期应该没有时区,所以我不确定为什么会进行调整.
这里有几个时区在运作:
Java的Date类(util和sql),它们具有UTC的隐式时区
JVM运行的时区,和
数据库服务器的默认时区.
所有这些都可以是不同的.Hibernate/JPA存在严重的设计缺陷,因为用户无法轻松确保在数据库服务器中保留时区信息(这允许在JVM中重建正确的时间和日期).
如果没有(轻松)使用JPA/Hibernate存储时区的能力,那么信息就会丢失,一旦信息丢失,构建它就会变得很昂贵(如果可能的话).
我认为最好始终存储时区信息(应该是默认值),然后用户应该具有优化时区的可选功能(尽管它只影响显示,但在任何日期仍然存在隐式时区).
对不起,这篇文章没有提供解决方法(已在其他地方得到解答),但这是为什么总是存储时区信息很重要的合理化.不幸的是,许多计算机科学家和编程从业者似乎只是因为他们不理解"信息丢失"的观点以及如何使国际化这样的事情变得非常困难而反对对时区的需求 - 这一天非常重要,因为网站可以访问客户和组织中的人们在世界各地移动.
使用Spring Boot JPA,在application.properties文件中使用以下代码,显然您可以根据自己的选择修改时区
spring.jpa.properties.hibernate.jdbc.time_zone = UTC
然后在您的Entity类文件中,
@Column private LocalDateTime created;