How to store date/time and timestamps in UTC time zone with JPA and Hibernate

如何将 JPA/Hibernate 配置为在数据库中以 UTC (GMT)时区的形式存储日期/时间?考虑一下这个带注释的 JPA 实体:

public class Event {
@Id
public int id;


@Temporal(TemporalType.TIMESTAMP)
public java.util.Date date;
}

如果日期是2008-2-03上午9:30太平洋标准时间(PST) ,那么我希望将2008-2-03下午5:30的 UTC 时间存储在数据库中。同样,当从数据库检索日期时,我希望将其解释为 UTC。所以在这种情况下,下午530点就是世界协调时下午530点。当它显示它将被格式化为太平洋标准时间上午9:30。

200391 次浏览

据我所知,您需要将整个 Java 应用程序放在 UTC 时区(这样 Hibernate 将以 UTC 格式存储日期) ,并且您需要在显示内容时转换到所需的任何时区(至少我们是这样做的)。

在创业初期,我们这样做:

TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));

并将所需的时区设置为 DateFormat:

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))

您可能会认为这个常见的问题将由 Hibernate 来解决。但它不是!有一些“技巧”可以让它变得正确。

我使用的方法是将 Date 作为 Long 存储在数据库中。所以我总是在1/1/70之后处理毫秒。然后,我的类上有只返回/接受日期的 getter 和 setter。因此 API 保持不变。缺点是我在数据库中有很长的时间。使用 SQL 时,我基本上只能执行 < ,> ,= 比较——而不是花哨的日期运算符。

另一种方法是使用自定义映射类型,如下所述: Http://www.hibernate.org/100.html

I think the correct way to deal with this is to use a Calendar instead of a Date though. With the Calendar you can set the TimeZone before persisting.

注意: 愚蠢的 stackoverflow 不让我评论,所以这里是对 davida 的回应。

如果你在芝加哥创建这个对象:

new Date(0);

Hibernate 将其保留为“12/31/196918:00:00”。日期应该没有时区,所以我不知道为什么会作出调整。

日期不在任何时区(它是一个毫秒办公室,从一个确定的时刻起,每个人的时间都是一样的) ,但是底层(R) DBs 通常以政治格式(年、月、日、小时、分钟、秒、 ...)存储时间戳,这是时区敏感的。

严肃地说,Hibernate 必须的允许在某种形式的映射中被告知 DB 日期处于某个时区,这样当它加载或存储它时,它就不会假设它自己..。

请看看我在 Sourceforge 上的项目,它有标准 SQL Date 和 Time 类型的用户类型以及 JSR 310和 Joda Time。所有类型都试图解决抵消问题。参见 http://sourceforge.net/projects/usertype/

编辑: 针对 Derek Mahar 在评论中提出的问题:

”克里斯,你的用户类型与 Hibernate 3或更高版本一起工作吗?- Derek Mahar Nov 7’10 at 12:30”

是的,这些类型支持 Hibernate 3.x 版本,包括 Hibernate 3.6。

Hibernate 对 Dates 中的时区内容一无所知(因为没有时区内容) ,但实际上是 JDBC 层导致了问题。ResultSet.getTimestampPreparedStatement.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。

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 上的注释。[我还没有测试过]

Hibernate 不允许通过注释或任何其他方式指定时区。如果使用 Calendar 而不是 date,则可以使用 HIbernate 属性 AccessType 实现解决方案,并自己实现映射。更高级的解决方案是实现自定义 UserType 来映射日期或日历。这两种解决方案在我的博客文章中都有解释: http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html

这里有几个时区:

  1. Java 的 Date 类(util 和 sql) ,它们具有隐式时区 协调世界时
  2. JVM 运行的时区,以及
  3. 数据库服务器的默认时区。

所有这些都可以是不同的。Hibernate/JPA 有一个严重的设计缺陷,用户不能很容易地确保时区信息保存在数据库服务器中(这允许在 JVM 中重建正确的时间和日期)。

如果不能(容易地)使用 JPA/Hibernate 存储时区,那么信息就会丢失,一旦信息丢失,构建它就会变得昂贵(如果有可能的话)。

我认为最好总是存储时区信息(应该是默认的) ,然后用户应该可以选择优化时区(尽管它只影响显示,但在任何日期中仍然有一个隐式的时区)。

对不起,这篇文章没有提供一个解决方案(已经在其他地方得到了回答) ,但它是一个为什么总是存储周围的时区信息是重要的合理化。不幸的是,似乎许多计算机科学家和编程从业者反对时区的需要,仅仅因为他们不欣赏“信息丢失”的观点,以及这如何使国际化这样的事情变得非常困难——这是非常重要的,如今的网站可访问的客户和人们在你的组织,因为他们在世界各地。

Adding an answer that's completely based on and indebted to divestoclimb with a hint from Shaun Stone. Just wanted to spell it out in detail since it's a common problem and the solution is a bit confusing.

这是使用 Hibernate 4.1.4。最终版,尽管我怀疑3.6之后的任何版本都可以。

首先,创建 diveoclam 的 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 );
}
};
}
}

然后创建 UtcTimestampType,它在超级构造函数调用中使用 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);
}
}

最后,在初始化 Hibernate 配置时,将 UtcTimestampType 注册为类型覆盖:

configuration.registerTypeOverride(new UtcTimestampType());

现在时间戳不应该关心 JVM 进出数据库的时区。

从 Hibernate 5.2开始,现在可以通过在 properties.xml JPA 配置文件中添加以下配置属性来强制使用 UTC 时区:

<property name="hibernate.jdbc.time_zone" value="UTC"/>

如果您正在使用 Spring Boot,那么将这个属性添加到您的 application.properties文件中:

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

当我想在 DB 中以 UTC 格式存储日期,避免使用 varchar和显式的 String <-> java.util.Date转换,或者在 UTC 时区设置整个 Java 应用程序时(因为如果 JVM 在许多应用程序中共享,这可能会导致另一个意想不到的问题) ,我遇到了同样的问题。

因此,有一个开源项目 DbAssist,它允许您轻松地将数据库中的读/写作为 UTC 日期。因为您使用 JPA 注释来映射实体中的字段,所有您需要做的就是在 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

The good thing is that you don't have to modify the entities at all; you can leave their java.util.Date fields as they are.

5.2.2必须与您正在使用的 Hibernate 版本相对应。我不确定您在项目中使用的是哪个版本,但是在项目的 Github的 wiki 页面上可以找到所提供的修复程序的完整列表。不同的 Hibernate 版本之所以有所不同,是因为 Hibernate 创建者在不同版本之间对 API 进行了多次更改。

在内部,该修复程序使用了来自 diveoclimm、 Shane 和一些其他来源的提示,以创建一个定制的 UtcDateType。然后,它将标准 java.util.Date与自定义 UtcDateType映射,后者处理所有必要的时区处理。 The mapping of the types is achieved using @Typedef annotation in the provided package-info.java file.

@TypeDef(name = "UtcDateType", defaultForType = Date.class, typeClass = UtcDateType.class),
package com.montrosesoftware.dbassist.types;

您可以找到一篇 给你文章,它解释了为什么会发生这样的时间转移,以及解决这个问题的方法是什么。

使用 Spring Boot JPA,在 application.properties 文件中使用以下代码,显然可以根据自己的选择修改时区

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

然后在您的实体类文件中,

@Column
private LocalDateTime created;