ChatGPT解决这个技术问题 Extra ChatGPT

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

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

V
Vadzim

从 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 中添加了对这个功能的支持?
哦,刚才我意识到你的个人资料和那些文章中的名字和图片是一样的……干得好,弗拉德 :)
@VladMihalcea 如果是用于 Mysql,则需要通过在连接字符串中使用 useTimezone=true 来告诉 MySql 使用时区。那么只有设置属性 hibernate.jdbc.time_zone 才会起作用
实际上,您需要将 useLegacyDatetimeCode 设置为 false
与 PostgreSQL 一起使用时,hibernate.jdbc.time_zone 似乎被忽略或无效
a
approxiblue

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

在启动时,我们会:

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

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

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

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

Hibernate 不知道 Dates 中的时区内容(因为没有),但实际上是 JDBC 层导致了问题。 ResultSet.getTimestampPreparedStatement.setTimestamp 在他们的文档中都说,在从/向数据库读取和写入数据时,默认情况下他们会将日期转换为当前 JVM 时区或从当前 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,与哪个版本的 Hibernate UtcTimestampType 兼容?
“ResultSet.getTimestamp 和 PreparedStatement.setTimestamp 在他们的文档中都说,在从数据库读取和写入数据库时,默认情况下它们会将日期转换到当前 JVM 时区或从当前 JVM 时区转换。”你有参考吗?对于这些方法,我在 Java 6 Javadocs 中没有看到任何提及。根据 stackoverflow.com/questions/4123534/…,这些方法如何将时区应用到给定的 DateTimestamp 取决于 JDBC 驱动程序。
我本可以发誓我去年读过关于使用 JVM 时区的文章,但现在我找不到了。我可能在特定 JDBC 驱动程序的文档中找到了它并进行了概括。
为了使您的示例的 3.6 版本正常工作,我必须创建一个新类型,它基本上是 TimeStampType 的包装器,然后在字段上设置该类型。
J
JavaGeek

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

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

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

@Column
private LocalDateTime created;

``` private Date lastUpdatedAt; 对我有用```
我真的很爱你
S
Shane

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


很高兴看到 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
c
codefinger

你会认为这个常见的问题会被 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”。日期应该没有时区,所以我不确定为什么要进行调整。


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

这里有几个时区在运行:

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

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

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

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

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


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

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


佚名

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

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


o
orim

当我想将数据库中的日期存储为 UTC 并避免使用 varchar 和显式 String <-> 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。

在内部,该修复程序使用了来自 divestoclimb、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,它解释了为什么会发生这种时间偏移以及解决它的方法。


O
Ole V.V.

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