119

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

4

11 回答 11

93

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

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

如果您使用的是 Spring Boot,则将此属性添加到您的application.properties文件中:

spring.jpa.properties.hibernate.jdbc.time_zone=UTC
于 2016-11-05T13:24:33.783 回答
56

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

在启动时,我们会:

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

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

fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
于 2009-02-03T18:44:32.230 回答
47

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");

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

于 2010-08-07T15:29:44.210 回答
26

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

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

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

@Column
private LocalDateTime created;
于 2019-03-27T15:58:19.183 回答
11

添加一个完全基于并感谢 divestoclimb 并得到 Shaun Stone 提示的答案。只是想详细说明它,因为这是一个常见问题,解决方案有点混乱。

这是使用 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。

于 2013-06-06T06:28:14.070 回答
10

你会认为这个常见的问题会被 Hibernate 解决。但它不是!有一些“技巧”可以让它正确。

我使用的一个是将日期作为 Long 存储在数据库中。所以我总是在 70 年 1 月 1 日之后以毫秒为单位工作。然后我在我的类上有 getter 和 setter 只返回/接受日期。所以 API 保持不变。不利的一面是我在数据库中很长。所以使用 SQL 我几乎只能做 <,>,= 比较——而不是花哨的日期运算符。

另一种方法是使用此处描述的自定义映射类型: http ://www.hibernate.org/100.html

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

注意:愚蠢的 stackoverflow 不允许我发表评论,所以这是对 david a 的回应。

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

new Date(0);

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

于 2009-02-17T17:33:51.030 回答
8

这里有几个时区在运行:

  1. Java 的 Date 类(util 和 sql),具有 UTC 的隐式时区
  2. 您的 JVM 正在运行的时区,以及
  3. 数据库服务器的默认时区。

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

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

我认为最好始终存储时区信息(应该是默认值),然后用户应该具有优化时区的可选能力(尽管它只会影响显示,但在任何日期仍然存在隐式时区)。

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

于 2010-12-13T21:07:45.103 回答
3

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

编辑:针对附在此评论中的 Derek Mahar 的问题:

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

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

于 2010-05-01T15:35:45.360 回答
2

日期不在任何时区(对于每个人来说,它是从定义的时间开始的毫秒办公室),但底层 (R)DB 通常以政治格式存储时间戳(年、月、日、小时、分钟、秒、. ..) 那是时区敏感的。

说真的,Hibernate必须允许在某种形式的映射中被告知 DB 日期在某某时区中,以便在加载或存储它时不会假定它是自己的……

于 2009-03-23T19:02:36.850 回答
1

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

于 2010-12-08T19:26:53.780 回答
1

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

因此,有一个开源项目DbAssist,它允许您轻松地将数据库中的读/写修复为 UTC 日期。由于您使用 JPA Annotations 来映射实体中的字段,因此您所要做的就是将以下依赖项包含到您的 Mavenpom文件中:

<dependency>
    <groupId>com.montrosesoftware</groupId>
    <artifactId>DbAssist-5.2.2</artifactId>
    <version>1.0-RELEASE</version>
</dependency>

@EnableAutoConfiguration然后通过在 Spring 应用程序类之前添加注释来应用修复(对于 Hibernate + Spring Boot 示例) 。有关其他设置安装说明和更多使用示例,请参阅项目的github

好处是您根本不必修改实体;您可以保持java.util.Date原样。

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

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

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

你可以在这里找到一篇文章,它解释了为什么会发生这种时间偏移以及解决它的方法是什么。

于 2016-11-08T13:31:32.043 回答