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

126

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


1
Vlad Mihalcea的回答提供了一个更新的答案(适用于Hibernate 5.2+)。 - Dinei
13个回答

113
自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

29
我也写了那个:D现在,猜猜是谁在Hibernate中添加了对此功能的支持? - Vlad Mihalcea
哦,刚才我意识到你在个人资料和那些文章中使用的名字和头像是一样的...做得好,弗拉德 :) - Dinei
1
如果是针对MySQL的话,需要在连接字符串中使用useTimezone=true来告诉MySQL使用时区。只有这样设置hibernate.jdbc.time_zone属性才会生效。 - The Coder
5
当与PostgreSQL一起使用时,hibernate.jdbc.time_zone似乎被忽略或没有效果。 - Alex R
如果您使用 Quarkus,您可以直接使用 quarkus属性 - Mr. Anderson
显示剩余5条评论

59
据我所知,您需要将整个Java应用程序放在UTC时区中(以便Hibernate将日期存储为UTC),并且在显示内容时需要将其转换为所需的任何时区(至少我们是这样做的)。
在启动时,我们执行以下操作:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));

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

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

6
mitchnull,你的解决方案并不适用于所有情况,因为Hibernate将日期设置委托给JDBC驱动程序,而每个JDBC驱动程序处理日期和时区的方式不同。请参见https://dev59.com/mW855IYBdhLWcg3w1n88#4124620。 - Derek Mahar
2
但是,如果我启动我的应用程序并通知JVM“-Duser.timezone=+00:00”属性,它的行为不同吗? - rafa.ferreira
8
据我所知,除非JVM和数据库服务器处于不同的时区,否则它在所有情况下都能正常运作。 - Shane
当jvm没有使用@Shane所说的相同时区时,这种方法无法正常工作。简单的下一个响应可以解决所有问题。 - Tony Chemit
还要在您的properties文件中添加以下内容,以确保数据库条目也在UTC中发生: spring.jpa.properties.hibernate.jdbc.time_zone=UTC - stranger
显示剩余2条评论

47
Hibernate 对 Date 中的时区信息是无感知的(因为没有时区信息),实际上造成问题的是 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中每个日期字段(还破坏了纯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
如何告诉 Hibernate 使用你自定义的 UtcTimestampType - Derek Mahar
2
"ResultSet.getTimestamp和PreparedStatement.setTimestamp在它们的文档中都说默认情况下将日期从/到当前JVM时区进行转换,读取和写入数据库。" 你有参考资料吗?我在Java 6 Javadocs中没有看到任何提到这些方法的内容。根据https://dev59.com/mW855IYBdhLWcg3w1n88#4124620,这些方法如何应用于给定的“Date”或“Timestamp”取决于JDBC驱动程序。 - Derek Mahar
1
我记得去年读过有关使用JVM时区的内容,但现在找不到了。可能是在特定JDBC驱动程序的文档中找到并概括了。 - divestoclimb
2
为了使你的示例工作在3.6版本上,我不得不创建一个新类型,它基本上是TimeStampType的包装器,然后将该类型设置在字段上。 - Shaun Stone
@divestoclimb,我有一个使用Hibernate 3.6的应用程序。我尝试了TimestampTypeDescriptor,但是我该如何将该描述符应用于单个实体字段?我尝试使用@Type(type="path.to.UtcTimestampTypeDescriptor")映射我的实体,但是会抛出_SQLGrammarException: could not execute query_异常。 - fl4l
显示剩余2条评论

26

使用Spring Boot JPA时,在您的application.properties文件中使用下面的代码,您可以自由地修改时区。

使用Spring Boot JPA时,在您的application.properties文件中使用下面的代码,您可以自由地修改时区。

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

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

@Column
private LocalDateTime created;

这对我有用:private Date lastUpdatedAt; - tiennv
1
我爱你如此深沉。 - Josean Muñoz
在实体中不应使用LocalDateTime,而应该使用OffsetDateTime(默认为UTC)。LocalDateTime使用本地时区设置,并且在使用1899年之前的日期时间时也会给出“错误”的日期时间。 - Serkan

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作为超类构造函数调用中的SqlTypeDescriptor,但除此之外将所有内容委派给TimestampType:

然后创建UtcTimestampType,它在超类构造函数调用中使用UtcTimestampTypeDescriptor作为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的时区相关。希望这对你有帮助。


6
希望看到JPA和Spring配置的解决方案。 - Aubergine
2
关于在Hibernate中使用本地查询的方法的注释。要使用这些重载类型,您必须使用query.setParameter(int pos,Object value)设置值,而不是query.setParameter(int pos,Date value,TemporalType temporalType)。如果使用后者,则Hibernate将使用其原始类型实现,因为它们是硬编码的。 - Nigel
我应该在哪里调用语句 configuration.registerTypeOverride(new UtcTimestampType());? - Stony
@Stony 无论你在哪里初始化你的Hibernate配置,如果你有一个HibernateUtil(大多数都有),它会在其中。 - Shane
1
它可以工作,但在检查后我意识到对于在时区="UTC"下工作并且所有默认时间戳类型为"带有时区的时间戳"(这是自动完成的)的postgres服务器来说是不必要的。但是,这里是修复版本,适用于Hibernate 4.3.5 GA,作为完整的单个类,并覆盖Spring工厂bean http://pastebin.com/tT4ACXn6 - Lukasz Frankowski
显示剩余3条评论

10
您可能认为这个常见问题会被Hibernate解决。但实际上没有!有一些"技巧"可以解决它。

我使用的方法是将日期作为长整型存储在数据库中。因此,我始终在处理从1970年1月1日开始的毫秒数。然后,在我的类中有仅返回/接受日期的getter和setter。因此,API保持不变。缺点是我在数据库中有长整型。因此,使用SQL基本上只能进行<,>,=比较--不能使用高级日期运算符。

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

我认为正确的处理方式是使用Calendar而不是Date。使用Calendar,您可以在持久化之前设置时区。

注意:愚蠢的stackoverflow不让我评论,所以这是对david a.的回复。

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

new Date(0);

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

1
真是我的错!你是对的,你帖子中的链接解释得很清楚。现在我想我的回答应该会受到一些负面声誉 :) - david a.
1
完全不是。你鼓励我发布一个非常明确的例子来说明这是一个问题。 - codefinger
4
按照你的建议,我能够使用 Calendar 对象正确地持久化时间并将它们存储在数据库中以 UTC 的形式。然而,当从数据库中读取持久化实体时,Hibernate 假设它们处于本地时区,并且 Calendar 对象是不正确的! - John K
1
为了解决这个“Calendar”读取问题,我认为Hibernate或JPA应该提供一些方式来指定每个映射的时区,以便Hibernate将其读取和写入到“TIMESTAMP”列中的日期进行转换。John K,请注意。 - Derek Mahar
joekutner,在阅读了https://dev59.com/mW855IYBdhLWcg3w1n88#4124620之后,我认同你的观点,我们应该在数据库中存储自纪元以来的毫秒数,而不是一个`Timestamp`,因为我们不能保证JDBC驱动程序按照我们期望的方式存储日期。 - Derek Mahar
@John K - 你能解释一下如何在数据库中存储UTC日期吗?你是否将JVM设置为在UTC上启动?这是我找到的唯一解决方案。 - rafa.ferreira

8
这里有几个时区需要考虑:
1. Java的日期类(util和sql),它们具有UTC的隐式时区。 2. 您的JVM运行的时区。 3. 数据库服务器的默认时区。
这些时区可能不同。Hibernate/JPA存在一个严重的设计缺陷,即用户不能轻松地确保时区信息在数据库服务器中得到保留(从而允许在JVM中重建正确的时间和日期)。
如果没有使用JPA/Hibernate轻松存储时区的能力,则会丢失信息,一旦信息丢失,则构造信息变得昂贵(如果可能的话)。
我认为最好始终存储时区信息(应该是默认值),然后用户应该有可选的能力来优化时区(尽管它只影响显示,在任何日期中仍有隐含的时区)。
抱歉,这篇文章没有提供解决方法(其他地方已经回答了),但它是对为什么始终存储时区信息很重要的合理化解释。不幸的是,似乎许多计算机科学家和编程实践者反对时区的需要,仅仅因为他们不理解“信息丢失”视角以及它如何使国际化变得非常困难——这在当今网站通过客户端和组织内部人员在全球范围内移动的情况下非常重要。

1
Hibernate/JPA存在严重的设计缺陷。我认为这是SQL的一个缺陷,传统上它允许时区是隐含的,因此可能是任何值。愚蠢的SQL。 - Raedwald
3
实际上,你可以不必总是存储时区信息,而是选择一个标准时区(通常为UTC),在持久化时将所有数据转换为该时区(读取时再转回原本的时区)。这也是我们通常的做法。但JDBC也不直接支持这种方式 :-/. - sleske

3
请查看我在Sourceforge上的项目,其中包含标准SQL日期和时间类型以及JSR 310和Joda Time的用户类型。所有类型都试图解决偏移问题。请参见http://sourceforge.net/projects/usertype/ 编辑:针对Derek Mahar在此评论中提出的问题:
“Chris,你的用户类型是否适用于Hibernate 3或更高版本?- Derek Mahar Nov 7 '10 at 12:30”
是的,这些类型支持Hibernate 3.x版本,包括Hibernate 3.6。

2

日期没有任何时区(它是从一个对于每个人都相同的定义时刻开始的毫秒偏移量),但基础的(R)DB通常以政治格式(年、月、日、小时、分、秒等)存储时间戳,这是时区敏感的。

认真地说,Hibernate必须允许通过某种形式的映射告知数据库日期处于某个特定的时区,以便在加载或存储数据时不会假设其自己的时区...


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接