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


106

如何配置JPA /休眠模式以UTC(GMT)时区在数据库中存储日期/时间?考虑以下带注释的JPA实体:

public class Event {
    @Id
    public int id;

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

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


1
Vlad Mihalcea的答案(针对Hibernate 5.2+)提供了更新的答案
Dinei'5

Answers:


73

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

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

有关更多详细信息,请查看本文


19
我也写了一个:D现在,猜猜谁在Hibernate中增加了对该功能的支持?
Vlad Mihalcea'5

哦,刚才我意识到您的个人资料和这些文章中的名称和图片都相同... Vlad好:)
Dinei

@VladMihalcea如果用于Mysql,则需要通过useTimezone=true在连接字符串中使用来告诉MySql使用时区。然后只有设置属性hibernate.jdbc.time_zone可以使用
TheCoder

实际上,您需要设置useLegacyDatetimeCode为false
Vlad Mihalcea

2
当与PostgreSQL一起使用时,hibernate.jdbc.time_zone似乎被忽略或不起作用
Alex R

48

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

在启动时,我们执行以下操作:

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

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

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

5
mitchnull,您的解决方案将无法在所有情况下都起作用,因为Hibernate将设置日期委托给JDBC驱动程序,并且每个JDBC驱动程序以不同的方式处理日期和时区。参见stackoverflow.com/questions/4123534/…
德里克·马哈

2
但是,如果我启动我的应用程序通知JVM“ -Duser.timezone = + 00:00”属性,会不会表现出同样的效果?
rafa.ferreira 2011年

8
据我所知,它将在所有情况下都有效,除非JVM和数据库服务器位于不同的时区。
Shane

stevekuo和@mitchnull看到了deepstoclimb解决方案,它下面的解决方案更好,并且有副作用证明stackoverflow.com/a/3430957/233906
Cerber

HibernateJPA是否支持“ @Factory”和“ @Externalizer”注释,这就是我在OpenJPA库中进行日期时间utc处理的方式。stackoverflow.com/questions/10819862/…–
Whome

44

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
您如何告诉Hibernate使用您的自定义UtcTimestampType
德里克·马哈

divestoclimb,与哪个版本的Hibernate UtcTimestampType兼容?
德里克·马哈尔

2
“ ResultSet.getTimestamp和PreparedStatement.setTimestamp都在他们的文档中说,默认情况下,当从数据库读写数据时,它们将日期转换为当前JVM时区。你有参考吗?在Java 6 Javadocs中,对于这些方法,我看不到任何提及。根据stackoverflow.com/questions/4123534/…的说明,这些方法如何将时区应用于给定时区DateTimestamp与JDBC驱动程序有关。
德里克·马哈尔

1
我可能发誓我去年读过有关使用JVM时区的文章,但现在找不到了。我可能已经在文档中为特定的JDBC驱动程序找到了它并进行了概括。
divestoclimb 2011年

2
为了使示例的3.6版本生效,我必须创建一个新类型,该类型基本上是TimeStampType的包装,然后在字段上设置该类型。
肖恩·斯通

17

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

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

然后在您的Entity类文件中,

@Column
private LocalDateTime created;

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。


6
非常高兴看到JPA和Spring Configuration解决方案。
茄子

2
关于在Hibernate中对本地查询使用这种方法的说明。要使用这些覆盖的类型,必须使用query.setParameter(int pos,Object value)而不是query.setParameter(int pos,Date value,TemporalTypetemporalType)设置值。如果使用后者,则Hibernate将使用其原始类型实现,因为它们是硬编码的。
奈杰尔

我应该在哪里调用语句configuration.registerTypeOverride(new UtcTimestampType()); ?
斯托尼2014年

@Stony无论您在何处初始化Hibernate配置。如果您有HibernateUtil(大多数情况下),它将在其中。
Shane 2014年

1
它可以工作,但是检查后我意识到,在timezone =“ UTC”中工作的postgres服务器并不需要所有默认类型的时间戳,都可以将其作为“带时区的时间戳”(然后自动完成)。但是,这里是Hibernate 4.3.5 GA的固定版本,作为完整的单个类,并且已覆盖spring factory bean pastebin.com/tT4ACXn6
Lukasz Frankowski

10

您会认为Hibernate将解决此常见问题。但事实并非如此!有一些“技巧”可以解决问题。

我使用的是将Date作为Long存储在数据库中。因此,我总是在1/1/70之后以毫秒为单位工作。然后,我的班级上会有一些getter和setter方法,它们只能返回/接受日期。因此,API保持不变。不利的一面是我对数据库很感兴趣。所以用SQL我几乎只能做<,>,=比较-不能做花哨的日期运算符。

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

我认为解决此问题的正确方法是使用日历而不是日期。使用日历,您可以在保留之前设置时区。

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

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

new Date(0);

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


1
真可惜!您说得对,帖子中的链接很好地说明了这一点。现在我想我的答案应该得到一些负面的声誉:)
大卫a。

1
一点也不。您鼓励我发布一个非常明确的示例,说明为什么这是一个问题。
codefinger

4
我能够使用Calendar对象正确地保存时间,以便按照您的建议将它们作为UTC存储在数据库中。但是,当从数据库读回持久化的实体时,Hibernate假定它们位于本地时区,并且Calendar对象不正确!
约翰·K

1
John K,为了解决此Calendar读取问题,我认为Hibernate或JPA应该为每种映射提供某种方式来指定Hibernate将其读取和写入的日期转换为TIMESTAMP列的时区。
德里克·马哈尔

joekutner,在阅读了stackoverflow.com/questions/4123534/…之后,我开始分享您的意见,即我们应该将自纪元以来的毫秒数存储在数据库中,而不是Timestamp因为我们不一定信任JDBC驱动程序将日期存储为我们会期望的。
德里克·马哈尔

8

这里有几个时区在运行:

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

所有这些都可以不同。Hibernate / JPA具有严重的设计缺陷,因为用户无法轻松地确保时区信息保留在数据库服务器中(这允许在JVM中重建正确的时间和日期)。

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

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

抱歉,这篇文章没有提供解决方法(在其他地方已回答),但这是为什么始终存储时区信息很重要的合理化解释。不幸的是,似乎许多计算机科学家和编程从业人员都反对时区的存在,只是因为他们不理解“信息丢失”的观点,以及这如何使诸如国际化之类的事情变得非常困难-这在当今很重要,因为如今人们可以访问网站了。客户和组织中的人员遍布世界各地。


1
“ Hibernate / JPA具有严重的设计缺陷”,我想说这是SQL的缺陷,传统上它允许时区是隐式的,因此可能是任何隐式的。愚蠢的SQL。
拉德瓦尔德

3
实际上,除了始终存储时区之外,您还可以标准化一个时区(通常是UTC),并在持久化时将所有内容转换为该时区(在读取时返回)。这就是我们通常要做的。但是,JDBC也不直接支持:-/。
sleske 2012年

3

请查看我在Sourceforge上的项目,该项目具有标准SQL日期和时间类型以及JSR 310和Joda Time的用户类型。所有类型都尝试解决偏移问题。参见http://sourceforge.net/projects/usertype/

编辑:针对此评论所附的德里克·马哈尔的问题:

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

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


2

日期不在任何时区中(每个人都在定义的时间点起是毫秒级的办公室),但是底层(R)DB通常以政治格式(年,月,日,时,分,秒,...)存储时间戳。 ..)对时区敏感。

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


1

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

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

<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进行了两次更改。

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

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

您可以在此处找到一篇文章其中解释了为什么会发生这种时移,以及解决该问题的方法。


1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.