如何使用JPA和Hibernate以UTC时区存储日期/时间和时间戳

时间:2009-02-03 17:15:51

标签: java hibernate datetime jpa timezone

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

public class Event {
    @Id
    public int id;

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

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

11 个答案:

答案 0 :(得分:49)

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

在启动时,我们会这样做:

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

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

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

答案 1 :(得分:47)

使用Hibernate 5.2,您现在可以使用以下配置属性强制使用UTC时区:

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

有关详细信息,请查看this article

答案 2 :(得分:41)

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

答案 3 :(得分:11)

根据肖恩·斯通的提示,添加一个完全的答案,并根据潜水员的情况加以评价。只是想详细说明一下,因为这是一个常见的问题而且解决方案有点令人困惑。

这是使用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");

    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进出数据库的时区。 HTH。

答案 4 :(得分:10)

您会认为这个常见问题会由Hibernate处理。但它不!为了做到这一点,有一些“黑客”。

我使用的是将Date作为Long存储在数据库中。所以我总是在1/1/70之后用毫秒工作。然后,我的班级上有getter和setter,只返回/接受日期。因此API保持不变。不好的一面是我在数据库中长期存在。因此,我只能做&lt;,&gt;,=比较 - 而不是花哨的日期运算符。

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

我认为处理此问题的正确方法是使用日历而不是日期。使用日历,您可以在持久化之前设置TimeZone。

注意:愚蠢的stackoverflow不会让我发表评论,所以这是对大卫a的回复。

如果您在芝加哥创建此对象:

new Date(0);

Hibernate将其保留为“12/31/1969 18:00:00”。日期应该没有时区,所以我不确定为什么会进行调整。

答案 5 :(得分:8)

这里有几个时区:

  1. Java的Date类(util和sql),它们具有隐式时区 UTC
  2. JVM运行的时区和
  3. 数据库服务器的默认时区。
  4. 所有这些都可能不同。 Hibernate / JPA存在严重的设计缺陷,因为用户无法轻松确保在数据库服务器中保留时区信息(这允许在JVM中重建正确的时间和日期)。

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

    我认为最好总是存储时区信息(应该是默认值),然后用户应该具有优化时区的可选功能(虽然它只影响显示,但任何时候都有隐含的时区)日期)。

    很抱歉,这篇文章没有提供解决方法(已在其他地方得到解决),但这是为什么总是存储时区信息很重要的合理化。不幸的是,许多计算机科学家和编程从业者似乎只是因为他们不理解“信息丢失”的观点以及如何使国际化这样的事情变得非常困难而反对对时区的需求 - 这一天非常重要,因为网站可以访问客户和组织中的人们在世界各地移动。

答案 6 :(得分:3)

请查看Sourceforge上的项目,该项目包含标准SQL日期和时间类型的用户类型以及JSR 310和Joda Time。所有类型都试图解决抵消问题。见http://sourceforge.net/projects/usertype/

编辑:回应Derek Mahar关于此评论的问题:

“Chris,您的用户类型是否适用于Hibernate 3或更高版本? - Derek Mahar 2010年11月7日12:30”

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

答案 7 :(得分:3)

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

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

然后在您的Entity类文件中,

@Column
private LocalDateTime created;

答案 8 :(得分:2)

日期不在任何时区(从每个人的定义时刻起是一个毫秒级办公室),但底层(R)DB通常以政治格式(年,月,日,小时,分钟,第二,......)这是时区敏感的。

说实话,Hibernate 必须允许在某种形式的映射中被告知DB日期在这样的时区中,以便在加载或存储它时它不会假设它自己...

答案 9 :(得分:1)

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

答案 10 :(得分:1)

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

因此,有一个开源项目DbAssist,它允许您轻松地将读/写作为UTC日期从数据库中修复。由于您使用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创建者在发行版之间多次更改了API。

在内部,修复程序使用来自divestoclimb,Shane和其他一些来源的提示,以创建自定义UtcDateType。然后,它将标准java.util.Date映射到自定义UtcDateType,后者处理所有必要的时区处理。 使用提供的@Typedef文件中的package-info.java注释来实现类型的映射。

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

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