如何使用JPA和Hibernate在UTC时区存储date/时间和时间戳
如何configurationJPA / Hibernate作为UTC(GMT)时区在数据库中存储date/时间? 考虑这个带注释的JPA实体:
public class Event { @Id public int id; @Temporal(TemporalType.TIMESTAMP) public java.util.Date date; }
如果date是2008年2月3日上午9:30太平洋标准时间(PST),那么我希望2008年2月3日下午5:30的UTC时间存储在数据库中。 同样,当从数据库中检索date时,我希望它解释为UTC。 所以在这种情况下,530pm是UTC530。 显示时,它将被格式化为太平洋标准时间上午9:30。
使用Hibernate 5.2,现在可以使用以下configuration属性来强制UTC时区:
<property name="hibernate.jdbc.time_zone" value="UTC"/>
有关更多详细信息,请参阅此文章 。
据我所知,你需要把你的整个Java应用程序放在UTC时区(这样Hibernate会以UTC来存储date),当你显示东西的时候,你需要把它转换成任何想要的时区(至less我们这样做这条路)。
在启动时,我们做:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
并将所需的时区设置为DateFormat:
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Hibernate不知道date中的时区(因为没有),但实际上是导致问题的JDBC层。 ResultSet.getTimestamp
和PreparedStatement.setTimestamp
都在他们的文档中声称,在从/向数据库读取和写入数据时,默认情况下它们将date从当前JVM时区转换为/从当前JVM时区转换。
我在Hibernate 3.5中通过子类org.hibernate.type.TimestampType
提出了一个解决scheme,强制这些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)); } }
如果您使用这些types,则应该修复TimeType和DateType。 缺点是你必须手动指定这些types将被用来代替POJO中每个date字段的默认值(并且打破纯JPA兼容性),除非有人知道更一般的覆盖方法。
更新:Hibernate 3.6已经改变了types的API。 在3.6中,我写了一个类UtcTimestampTypeDescriptor来实现这个。
public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicBinder<X>( 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 <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicExtractor<X>( 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上的注释。 [我还没有testing过]
你会认为这个常见的问题将由Hibernate来处理。 但它不是! 有几个“黑客”来解决问题。
我使用的是在数据库中存储date为长。 所以我总是在1970年1月1日以后以毫秒为单位工作。 然后我在我的class上有getter和setter,返回/只接受date。 所以API保持不变。 缺点是我在数据库中有很长一段时间。 所以用SQL我几乎只能做<,>,=比较 – 不是花哨的date操作符。
另一种方法是按照此处所述使用自定义映射types: http : //www.hibernate.org/100.html
我认为处理这个问题的正确方法是使用日历,而不是date。 使用日历,您可以在持续之前设置时区。
注意:愚蠢的计算器不会让我评论,所以这里是对大卫a的回应。
如果你在芝加哥创build这个对象:
new Date(0);
Hibernate坚持为“12/31/1969 18:00:00”。 date应该没有时区,所以我不知道为什么要进行调整。
添加一个完全基于和负债剥离的答案与肖恩斯通暗示。 只是想详细说明,因为这是一个普遍的问题,解决方法有点混乱。
这是使用Hibernate 4.1.4.Final,虽然我怀疑3.6以后的任何工作。
首先,创builddivestoclimb的UtcTimestampTypeDescriptor
public class UtcTimestampTypeDescriptor extends TimestampTypeDescriptor { public static final UtcTimestampTypeDescriptor INSTANCE = new UtcTimestampTypeDescriptor(); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicBinder<X>( 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 <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicExtractor<X>( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { return javaTypeDescriptor.wrap( rs.getTimestamp( name, Calendar.getInstance(UTC) ), options ); } }; } }
然后创buildUtcTimestampType,它使用UtcTimestampTypeDescriptor而不是TimestampTypeDescriptor作为超级构造函数调用中的SqlTypeDescriptor,否则将一切都委托给TimestampType:
public class UtcTimestampType extends AbstractSingleColumnStandardBasicType<Date> implements VersionType<Date>, LiteralType<Date> { 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<Date> 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); } }
最后,当你初始化你的Hibernateconfiguration时,注册UtcTimestampType作为一个types覆盖:
configuration.registerTypeOverride(new UtcTimestampType());
现在,时间戳不应该关注JVM的时区到数据库的path。 HTH。
这里有几个时区:
- Java的Date类(util和sql),它们具有UTC的隐式时区
- 您的JVM正在运行的时区,以及
- 数据库服务器的默认时区。
所有这些可以是不同的。 Hibernate / JPA有一个严重的devise缺陷,用户不能容易地确保时区信息被保存在数据库服务器中(这允许在JVM中重build正确的时间和date)。
如果没有使用JPA / Hibernate(轻松)存储时区的function,那么信息就会丢失,一旦信息丢失,构build它就变得非常昂贵(如果可能的话)。
我认为最好总是存储时区信息(应该是默认值),然后用户可以select优化时区(尽pipe它只影响显示,在任何date仍然有隐含的时区)。
对不起,这篇文章没有提供解决方法(这在其他地方已经得到解答),但是为什么始终存储时区信息非常重要。 不幸的是,似乎很多计算机科学家和编程从业者反对时区的需要,仅仅是因为他们不了解“信息损失”的观点,以及如何使国际化这样的事情非常困难 – 这是非常重要的今天,网站访问客户和您的组织中的人员在世界各地移动。
请看看我的Sourceforge项目,它具有标准SQLdate和时间types以及JSR 310和Joda Time的用户types。 所有types都试图解决抵消问题。 参见http://sourceforge.net/projects/usertype/
编辑:在回应德里克·马哈尔的问题附加到这个评论:
“克里斯,你的用户types使用Hibernate 3或更高? – Derek Mahar 11年7月7日在12:30”
是的,这些types支持Hibernate 3.x版本,包括Hibernate 3.6。
date不在任何时区(从每个人定义的时刻开始,这是一个毫秒级的办公室),但底层(R)数据库通常以政治格式(年,月,日,时,分,秒)存储时间戳。 ..)是时区敏感的。
认真的说,Hibernate 必须允许被告知以某种forms的映射,DBdate是在这样的时间,以便当它加载或存储它不承担自己的… …
hibernate不允许通过注释或其他方式指定时区。 如果您使用日历而不是date,则可以使用HIbernate属性AccessType实施解决方法并自行实施映射。 更高级的解决scheme是实现一个自定义的UserType来映射您的date或日历。 这个博客文章解释了两种解决scheme: http : //dev-metal.blogspot.com/2010/11/mapping-dates-and-time-zones-with.html
当我想将date作为UTC存储在数据库中时,我遇到了同样的问题,并且避免使用varchar
和显式的String <-> java.util.Date
转换,或者在UTC时区设置我的整个Java应用程序导致另一个意想不到的问题,如果JVM在许多应用程序中共享)。
所以,有一个开源项目DbAssist
,它允许你很容易地修改从数据库的UTCdate的读/写。 由于您使用的是JPA Annotations来映射实体中的字段,所以您只需要将以下依赖项包含到Maven pom
文件中:
<dependency> <groupId>com.montrosesoftware</groupId> <artifactId>DbAssist-5.2.2</artifactId> <version>1.0-RELEASE</version> </dependency>
然后通过在Spring应用程序类之前添加@EnableAutoConfiguration
注释来应用修复(对于Hibernate + Spring Boot示例)。 对于其他安装说明和更多使用示例,请参阅项目的github 。
好的是你根本不需要修改实体; 你可以保留它们的java.util.Date
字段。
5.2.2
必须对应于你正在使用的Hibernate版本。 我不确定,您在项目中使用的是哪个版本,但是项目的github的wiki页面上提供了完整的提供的修复列表。 Hibernate的各种版本之所以解决这个问题的原因是因为Hibernate的创build者在这两个版本之间多次改变了API。
在内部,修复使用来自divestoclimb,Shane和一些其他来源的提示来创build一个自定义的UtcDateType
。 然后它将标准java.util.Date
映射到处理所有必要的时区处理的自定义UtcDateType
。 types的映射是通过在提供的package-info.java
文件中使用@Typedef
注释来实现的。
@TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class), package com.montrosesoftware.dbassist.types;
你可以在这里find一篇文章,解释为什么会出现这样的时间转换,以及解决这个问题的方法是什么。