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

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

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

1
2
3
4
5
6
7
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时区:

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

有关更多详细信息,请查看此文章。


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

在启动时,我们做:

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

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

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


Hibernate对Dates中的时区内容一无所知(因为没有),但它实际上是导致问题的JDBC层。 ResultSet.getTimestampPreparedStatement.setTimestamp都在他们的文档中说,默认情况下,当从/向数据库读取和写入时,它们会将日期转换为当前JVM时区。

我通过子类化org.hibernate.type.TimestampType在Hibernate 3.5中提出了一个解决方案,强制这些JDBC方法使用UTC而不是本地时区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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来实现它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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上的注释。 [我还没有测试过这个]


通过Shaun Stone的提示添加一个完全基于并对于divestoclimb负债的答案。只是想详细说明,因为这是一个常见的问题,解决方案有点令人困惑。

这是使用Hibernate 4.1.4.Final,虽然我怀疑3.6之后的任何东西都可以工作。

首先,创建divestoclimb的UtcTimestampTypeDescriptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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注册为类型覆盖:

1
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。

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

1
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文件中使用以下代码,显然您可以根据自己的选择修改时区

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

    然后在您的Entity类文件中

    1
    2
    @Column
    private LocalDateTime created;

    请查看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。


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

    要认真一点,Hibernate必须允许在某种形式的映射中被告知DB日期在这样的时区中,这样当它加载或存储它时它不会假设它自己......


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

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

    1
    2
    3
    4
    5
    <dependency>
        <groupId>com.montrosesoftware</groupId>
        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,后者处理所有必要的时区处理。
    使用提供的package-info.java文件中的@Typedef注释实现类型的映射。

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

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


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