如何配置 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。
从 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
据我所知,您需要将整个 Java 应用程序置于 UTC 时区(以便 Hibernate 以 UTC 存储日期),并且您需要在显示内容时转换为所需的任何时区(至少我们这样做这边走)。
在启动时,我们会:
TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
并将所需的时区设置为 DateFormat:
fmt.setTimeZone(TimeZone.getTimeZone("Europe/Budapest"))
Hibernate 不知道 Dates 中的时区内容(因为没有),但实际上是 JDBC 层导致了问题。 ResultSet.getTimestamp
和 PreparedStatement.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 上的注释。 [我还没有测试过这个]
UtcTimestampType
?
UtcTimestampType
兼容?
Date
或 Timestamp
取决于 JDBC 驱动程序。
使用 Spring Boot JPA,在 application.properties 文件中使用以下代码,显然您可以根据自己的选择修改时区
spring.jpa.properties.hibernate.jdbc.time_zone = UTC
然后在您的实体类文件中,
@Column
private LocalDateTime created;
添加一个完全基于并感谢 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。
你会认为这个常见的问题会被 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
读取问题,我认为 Hibernate 或 JPA 应该提供一些方法来指定,对于每个映射,Hibernate 应该将它读取和写入到 TIMESTAMP
的日期转换到的时区柱子。
Timestamp
,因为我们不一定相信 JDBC 驱动程序会像我们期望的那样存储日期.
这里有几个时区在运行:
Java 的 Date 类(util 和 sql),具有 UTC 的隐式时区 JVM 正在运行的时区,以及数据库服务器的默认时区。
所有这些都可能不同。 Hibernate/JPA 有一个严重的设计缺陷,即用户不能轻易确保时区信息保存在数据库服务器中(这允许在 JVM 中重建正确的时间和日期)。
如果无法(轻松)使用 JPA/Hibernate 存储时区,那么信息就会丢失,一旦信息丢失,构建它就会变得昂贵(如果可能的话)。
我认为最好始终存储时区信息(应该是默认值),然后用户应该具有优化时区的可选能力(尽管它只会影响显示,但在任何日期仍然存在隐式时区)。
抱歉,这篇文章没有提供解决方法(已在其他地方得到回答),但它是为什么总是存储时区信息很重要的合理化。不幸的是,似乎许多计算机科学家和编程从业者反对时区的必要性,仅仅是因为他们不理解“信息丢失”的观点,以及这如何使国际化等事情变得非常困难——这对于现在可以访问的网站非常重要您组织中的客户和人员在世界各地移动时。
请查看我在 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 必须允许在某种形式的映射中被告知数据库日期在某某时区,这样当它加载或存储它时,它不会假定它自己...
当我想将数据库中的日期存储为 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,它解释了为什么会发生这种时间偏移以及解决它的方法。
Hibernate 不允许通过注释或任何其他方式指定时区。如果您使用日历而不是日期,则可以使用 HIbernate 属性 AccessType 实现解决方法并自己实现映射。更高级的解决方案是实现自定义 UserType 来映射您的日期或日历。这两种解决方案都在我的博文中进行了解释:http://www.joobik.com/2010/11/mapping-dates-and-time-zones-with.html
不定期副业成功案例分享
useTimezone=true
来告诉 MySql 使用时区。那么只有设置属性hibernate.jdbc.time_zone
才会起作用useLegacyDatetimeCode
设置为 false