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

Posted

技术标签:

【中文标题】如何使用 JPA 和 Hibernate 在 UTC 时区中存储日期/时间和时间戳【英文标题】:How to store date/time and timestamps in UTC time zone with JPA and Hibernate 【发布时间】:2010-10-05 05:08:25 【问题描述】:

如何配置 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。

【问题讨论】:

Vlad Mihalcea's answer 提供了更新的答案(针对 Hibernate 5.2+) 【参考方案1】:

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

在启动时,我们这样做:

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

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

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

【讨论】:

mitchnull,您的解决方案并非在所有情况下都有效,因为 Hibernate 将设置日期委托给 JDBC 驱动程序,并且每个 JDBC 驱动程序处理日期和时区的方式不同。见***.com/questions/4123534/…。 但是如果我启动我的应用程序通知 JVM "-Duser.timezone=+00:00" 属性,行为不一样吗? 据我所知,它适用于所有情况,除非 JVM 和数据库服务器位于不同的时区。 stevekuo 和@mitchnull 请参阅下面的divestoclimb 解决方案,哪个更好副作用证明***.com/a/3430957/233906 HibernateJPA 是否支持“@Factory”和“@Externalizer”注释,这就是我在 OpenJPA 库中处理日期时间 utc 的方式。 ***.com/questions/10819862/…【参考方案2】:

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

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

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

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

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

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

new Date(0);

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

【讨论】:

真丢脸!您是对的,您帖子中的链接很好地解释了这一点。现在我想我的答案应该得到一些负面的声誉:) 一点也不。你鼓励我发布一个非常明确的例子来说明为什么这是一个问题。 我能够使用 Calendar 对象正确保存时间,以便按照您的建议将它们作为 UTC 存储在数据库中。但是,当从数据库中读回持久化实体时,Hibernate 会假定它们处于本地时区并且 Calendar 对象不正确! John K,为了解决这个 Calendar 读取问题,我认为 Hibernate 或 JPA 应该提供一些方法来指定,对于每个映射,Hibernate 应该将它转换到的时区读取和写入 TIMESTAMP 列。 joekutner,在阅读了***.com/questions/4123534/… 之后,我来分享您的意见,我们应该在数据库中存储自 Epoch 以来的毫秒数而不是 Timestamp,因为我们不一定信任 JDBC驱动程序按照我们的预期存储日期。【参考方案3】:

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

严肃地说,Hibernate必须被允许在某种形式的映射中被告知数据库日期在某某时区,这样当它加载或存储它时,它不会假定它的拥有...

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

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

【讨论】:

你如何告诉 Hibernate 使用你的自定义 UtcTimestampType divestoclimb,UtcTimestampType 与哪个版本的 Hibernate 兼容? "ResultSet.getTimestamp 和 PreparedStatement.setTimestamp 在他们的文档中都说,在从数据库读取和写入数据库时​​,默认情况下它们会将日期转换到当前 JVM 时区。"你有参考吗?对于这些方法,我在 Java 6 Javadocs 中没有看到任何提及。根据***.com/questions/4123534/…,这些方法如何将时区应用于给定的DateTimestamp 取决于JDBC 驱动程序。 我可以发誓我去年读到过关于使用 JVM 时区的文章,但现在我找不到了。我可能在特定 JDBC 驱动程序的文档中找到了它并进行了概括。 为了让你的示例的 3.6 版本工作,我必须创建一个新类型,它基本上是 TimeStampType 的包装器,然后在字段上设置该类型。【参考方案6】:

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

【讨论】:

【参考方案7】:

这里有几个时区在运行:

    Java 的 Date 类(util 和 sql),具有隐式时区 世界标准时间 运行 JVM 的时区,以及 数据库服务器的默认时区。

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

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

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

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

【讨论】:

“Hibernate/JPA 有一个严重的设计缺陷”我会说这是 SQL 的一个缺陷,传统上它允许时区是隐式的,因此可能是任何东西。愚蠢的 SQL。 实际上,除了总是存储时区之外,您还可以标准化一个时区(通常是 UTC),并在持久化时将所有内容转换为该时区(并在读取时返回)。这是我们通常做的。但是,JDBC 也不直接支持:-/.【参考方案8】:

添加一个完全基于并感谢在 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。

【讨论】:

很高兴看到 JPA 和 Spring 配置的解决方案。 关于在 Hibernate 中将这种方法与本机查询一起使用的说明。要使用这些覆盖类型,您必须使用 query.setParameter(int pos, Object value) 设置值,而不是 query.setParameter(int pos, Date value, TemporalType temporalType)。如果您使用后者,那么 Hibernate 将使用其原始类型实现,因为它们是硬编码的。 我应该在哪里调用语句 configuration.registerTypeOverride(new UtcTimestampType()); ? @Stony 无论您在哪里初始化 Hibernate 配置。如果你有一个 HibernateUtil(大多数都有),它就会在那里。 它可以工作,但在检查后我意识到在 timezone="UTC" 中工作的 postgres 服务器不需要它,并且所有默认类型的时间戳都为“带时区的时间戳”(它会自动完成) )。但是,这里是 Hibernate 4.3.5 GA 的固定版本,作为完整的单类和覆盖 spring 工厂 bean pastebin.com/tT4ACXn6【参考方案9】:

从 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

【讨论】:

我也写了一个 :D 现在,猜猜是谁在 Hibernate 中添加了对这个功能的支持? 哦,刚才我意识到你的个人资料和那些文章中的名字和图片是一样的......干得好 Vlad :) @VladMihalcea 如果是用于 mysql,则需要通过在连接字符串中使用 useTimezone=true 来告诉 MySql 使用时区。那么只有设置属性hibernate.jdbc.time_zone 才能工作 其实需要将useLegacyDatetimeCode设置为false hibernate.jdbc.time_zone 与 PostgreSQL 一起使用时似乎被忽略或无效【参考方案10】:

当我想将数据库中的日期存储为 UTC 并避免使用 varchar 和显式 String &lt;-&gt; java.util.Date 转换,或者将我的整个 Java 应用程序设置在 UTC 时区时,我遇到了同样的问题(因为这可能会导致如果 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。

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

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

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

【讨论】:

【参考方案11】:

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

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

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

@Column
private LocalDateTime created;

【讨论】:

这对我有用 ``` private Date lastUpdatedAt; ```

以上是关于如何使用 JPA 和 Hibernate 在 UTC 时区中存储日期/时间和时间戳的主要内容,如果未能解决你的问题,请参考以下文章

如何将继承策略与 JPA 注释和 Hibernate 混合使用?

如何在 JPA 和 Hibernate 中返回所有值

如何使用 JPA 和 Hibernate 映射计算的属性

如何使用 JPA 和 Hibernate 防止 SQL 注入?

如何使用 JPA 和 Hibernate 修复 StaleObjectStateException

如何使用 Hibernate 和 JPA 处理填充下拉列表?