如何定义时区处理
在Hibernate 6中,你可以通过两种方式定义时区处理。
1.你可以通过在你的persistence.xml中设置配置属性hibernate.timezone.default_storage属性来指定一个默认的处理。TimeZoneStorageType枚举定义了支持的配置值,我将在下一节详细讨论:
<persistence>
<persistence-unit name="my-persistence-unit">
<description>Hibernate example configuration - thorben-janssen.com</description>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="hibernate.timezone.default_storage" value="NORMALIZE"/>
...
</properties>
</persistence-unit>
</persistence>
2.你可以通过用*@TimeZoneStorage注解它并提供一个TimeZoneStorageType* 枚举值来定制每个ZonedDateTime或OffsetDateTime类型的实体属性的时区处理。
@Entity
public class ChessGame {
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private ZonedDateTime zonedDateTime;
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private OffsetDateTime offsetDateTime;
...
}
5种不同的时区存储类型
你可以选择5种不同的选项来存储时区信息。它们告诉Hibernate将时间戳存储在TIMESTAMP_WITH_TIMEZONE类型的列中,将时间戳和时区持久化在两个独立的列中,或者将时间戳规范化到不同的时区。我将在下面的章节中向你展示所有映射的例子以及Hibernate如何处理它们。
所有的例子都将以这个简单的ChessGame 实体类为基础。属性ZonedDateTime zonedDateTime和OffsetDateTime offsetDateTime将存储游戏进行的日期和时间。
@Entity
public class ChessGame {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private ZonedDateTime zonedDateTime;
private OffsetDateTime offsetDateTime;
private String playerWhite;
private String playerBlack;
@Version
private int version;
...
}
而我将使用这个测试案例来持久化一个新的ChessGame 实体对象。它将zonedDateTime 和offsetDateTime属性设置为2022-04-06 15:00 +04:00。在我持久化了实体后,我提交了事务,开始了一个新的事务,并从数据库中获取了同一个实体。
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
ZonedDateTime zonedDateTime = ZonedDateTime.of(2022, 4, 6, 15, 0, 0, 0, ZoneId.of("UTC+4"));
OffsetDateTime offsetDateTime = OffsetDateTime.of(2022, 4, 6, 15, 0, 0, 0, ZoneOffset.ofHours(4));
ChessGame game = new ChessGame();
game.setPlayerWhite("Thorben Janssen");
game.setPlayerBlack("A better player");
game.setZonedDateTime(zonedDateTime);
game.setOffsetDateTime(offsetDateTime);
em.persist(game);
em.getTransaction().commit();
em.close();
em = emf.createEntityManager();
em.getTransaction().begin();
ChessGame game2 = em.find(ChessGame.class, game.getId());
assertThat(game2.getZonedDateTime()).isEqualTo(zonedDateTime);
assertThat(game2.getOffsetDateTime()).isEqualTo(offsetDateTime);
em.getTransaction().commit();
em.close();
让我们仔细看看所有5个TimeZoneStorageType 选项。
TimeZoneStorageType.NATIVE
警告:当我使用h2数据库准备本文的例子时,Hibernate使用了列timestamp(6),而不是带有时区的timestamp。请仔细检查Hibernate是否使用了正确的列类型。
下面的部分描述了预期的行为。
当配置TimeZoneStorageType.NATIVE时,Hibernate将时间戳存储在TIMESTAMP_WITH_TIMEZONE类型的列中。这个列的类型必须被你的数据库所支持。
@Entity
public class ChessGame {
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private ZonedDateTime zonedDateTime;
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private OffsetDateTime offsetDateTime;
...
}
在这种情况下,所有读操作的处理都很简单,与其他基本属性类型的处理没有区别。数据库会存储带有时区信息的时间戳。Hibernate只需要设置一个ZonedDateTime或OffsetDateTime 对象作为绑定参数或从结果集中提取。
13:10:55,725 DEBUG [org.hibernate.SQL] - insert into ChessGame (offsetDateTime, playerBlack, playerWhite, version, zonedDateTime, id) values (?, ?, ?, ?, ?, ?)
13:10:55,727 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [TIMESTAMP] - [2022-04-06T15:00+04:00]
13:10:55,735 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [VARCHAR] - [A better player]
13:10:55,735 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [Thorben Janssen]
13:10:55,736 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [INTEGER] - [0]
13:10:55,736 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [TIMESTAMP] - [2022-04-06T15:00+04:00[UTC+04:00]]
13:10:55,736 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [BIGINT] - [1]
...
13:10:55,770 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.offsetDateTime,c1_0.playerBlack,c1_0.playerWhite,c1_0.version,c1_0.zonedDateTime from ChessGame c1_0 where c1_0.id=?
...
13:10:55,785 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [1] - [2022-04-06T13:00+02:00]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [2] - [A better player]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [3] - [Thorben Janssen]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]
13:10:55,786 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [5] - [2022-04-06T13:00+02:00[Europe/Berlin]]
13:10:55,787 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]
TimeZoneStorageType.NORMALIZE
TimeZoneStorageType.NORMALIZE 与Hibernate 5提供的处理方式相同,是Hibernate 6的默认选项。
@Entity
public class ChessGame {
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
private ZonedDateTime zonedDateTime;
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE)
private OffsetDateTime offsetDateTime;
...
}
它告诉Hibernate让JDBC驱动将时间戳规范化为其本地时区或hibernate.jdbc.time_zone设置中定义的时区。然后,它在数据库中存储不带时区信息的时间戳。
当你记录INSERT语句的绑定参数值时,你就看不到这一点了。Hibernate在这里仍然使用你的实体对象的属性值。
11:44:00,815 DEBUG [org.hibernate.SQL] - insert into ChessGame (offsetDateTime, playerBlack, playerWhite, version, zonedDateTime, id) values (?, ?, ?, ?, ?, ?)
11:44:00,819 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [TIMESTAMP] - [2022-04-06T15:00+04:00]
11:44:00,838 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [VARCHAR] - [A better player]
11:44:00,839 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [Thorben Janssen]
11:44:00,839 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [INTEGER] - [0]
11:44:00,839 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [TIMESTAMP] - [2022-04-06T15:00+04:00[UTC+04:00]]
11:44:00,840 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [BIGINT] - [1]
但是ResourceRegistryStandardImpl 类的跟踪记录提供了更多关于已执行的准备语句的信息。在那里,你可以看到Hibernate将时间戳从2022-04-06 15:00+04:00规范化为我的本地时区(UTC+2),并移除时区偏移2022-04-06 13:00:00。
11:44:46,247 TRACE [org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl] - Closing prepared statement [prep3: insert into ChessGame (offsetDateTime, playerBlack, playerWhite, version, zonedDateTime, id) values (?, ?, ?, ?, ?, ?) {1: TIMESTAMP '2022-04-06 13:00:00', 2: 'A better player', 3: 'Thorben Janssen', 4: 0, 5: TIMESTAMP '2022-04-06 13:00:00', 6: 1}]
当Hibernate从数据库中读取时间戳时,JDBC驱动会得到没有时区信息的时间戳,并添加其时区或由hibernate.jdbc.time_zone设置定义的时区。
11:55:17,225 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.offsetDateTime,c1_0.playerBlack,c1_0.playerWhite,c1_0.version,c1_0.zonedDateTime from ChessGame c1_0 where c1_0.id=?
11:55:17,244 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [1] - [2022-04-06T13:00+02:00]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [2] - [A better player]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [3] - [Thorben Janssen]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]
11:55:17,245 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [5] - [2022-04-06T13:00+02:00[Europe/Berlin]]
11:55:17,247 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [0]
正如你在日志输出中看到的,Hibernate从数据库中选择了ChessGame实体对象,并检索到正确的时间戳。但是由于执行了规范化,它不再是我持久化实体时使用的时区UTC+4。为了避免任何时区转换,你需要使用*TimeZoneStorageType.NATIVE或TimeZoneStorageType.COLUMN*。
时间戳规范化可能是有风险的
如果你的数据库不支持TIMESTAMP_WITH_TIMEZONE的列类型,将你的时间戳规范化并在没有时区信息的情况下存储它们可能看起来是一个简单而明显的解决方案。但它引入了两个风险。
- 改变你的本地时区或在不同的时区运行服务器会影响反常化,导致错误的数据。
- 有夏令时的时区不能被安全地规范化,因为它们有一个小时存在于夏季和冬季。通过删除时区信息,你不能再区分夏季和冬季,因此,你不能正确地规范该时期的任何时间戳。为了避免这种情况,你应该总是使用没有DST的时区,例如UTC。
TimeZoneStorageType.NORMALIZE_UTC
警告:如HHH-15174所述,Hibernate 6.0.0.Final不会将你的时间戳规范化为UTC,而是应用与TimeZoneStorageType.NORMALIZE相同的规范化。
下面的部分描述了预期的行为。
TimeZoneStorageType.NORMALIZE_UTC与之前讨论的TimeZoneStorageType.NORMALIZE非常相似。唯一的区别是,你的时间戳被规范化为UTC,而不是JDBC驱动的时区或配置为hibernate.jdbc.time_zone的时区。
@Entity
public class ChessGame {
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC)
private ZonedDateTime zonedDateTime;
@TimeZoneStorage(TimeZoneStorageType.NORMALIZE_UTC)
private OffsetDateTime offsetDateTime;
...
}
Hibernate对时间戳的处理以及在读写操作中执行的规范化与TimeZoneStorageType.NORMALIZE相同,我在上一节中详细解释了这一点。
TimeZoneStorageType.COLUMN
当配置TimeZoneStorageType.COLUMN时,Hibernate将不含时区信息的时间戳和时区与UTC的偏移量分别存储在不同的数据库列中。
@Entity
public class ChessGame {
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "zonedDateTime_zoneOffset")
private ZonedDateTime zonedDateTime;
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "offsetDateTime_zoneOffset")
private OffsetDateTime offsetDateTime;
...
}
Hibernate使用其命名策略,将ZonedDateTime 或OffsetDateTime 类型的实体属性映射到数据库列中。这一列存储的是时间戳。默认情况下,Hibernate会在该列的名称中添加后缀*_tz* ,以获得包含时区偏移的列的名称。你可以通过用*@TimeZoneColumn*来注解你的实体属性来定制这一点,就像我在前面的代码片段中做的那样。
当你坚持一个新的ChessGame实体对象并使用我推荐的开发环境的日志配置时,你可以清楚地看到这种处理。
12:31:45,654 DEBUG [org.hibernate.SQL] - insert into ChessGame (offsetDateTime, offsetDateTime_zoneOffset, playerBlack, playerWhite, version, zonedDateTime, zonedDateTime_zoneOffset, id) values (?, ?, ?, ?, ?, ?, ?, ?)
12:31:45,656 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [1] as [TIMESTAMP_UTC] - [2022-04-06T11:00:00Z]
12:31:45,659 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [2] as [INTEGER] - [+04:00]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [3] as [VARCHAR] - [A better player]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [4] as [VARCHAR] - [Thorben Janssen]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [5] as [INTEGER] - [0]
12:31:45,660 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [6] as [TIMESTAMP_UTC] - [2022-04-06T11:00:00Z]
12:31:45,661 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [7] as [INTEGER] - [+04:00]
12:31:45,661 TRACE [org.hibernate.orm.jdbc.bind] - binding parameter [8] as [BIGINT] - [1]
基于时间戳和时区偏移,Hibernate在从数据库中获取实体对象时,会实例化一个新的OffsetDateTime或ZonedDateTime 对象。
12:41:26,082 DEBUG [org.hibernate.SQL] - select c1_0.id,c1_0.offsetDateTime,c1_0.offsetDateTime_zoneOffset,c1_0.playerBlack,c1_0.playerWhite,c1_0.version,c1_0.zonedDateTime,c1_0.zonedDateTime_zoneOffset from ChessGame c1_0 where c1_0.id=?
...
12:41:26,094 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Initializing composite instance [com.thorben.janssen.sample.model.ChessGame.offsetDateTime]
12:41:26,107 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [1] - [2022-04-06T11:00:00Z]
12:41:26,108 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [2] - [+04:00]
12:41:26,109 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Created composite instance [com.thorben.janssen.sample.model.ChessGame.offsetDateTime] : 2022-04-06T15:00+04:00
...
12:41:26,109 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Initializing composite instance [com.thorben.janssen.sample.model.ChessGame.zonedDateTime]
12:41:26,110 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [6] - [2022-04-06T11:00:00Z]
12:41:26,110 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [7] - [+04:00]
12:41:26,110 DEBUG [org.hibernate.orm.results.loading.org.hibernate.orm.results.loading.embeddable] - Created composite instance [com.thorben.janssen.sample.model.ChessGame.zonedDateTime] : 2022-04-06T15:00+04:00
...
12:41:26,112 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [3] - [A better player]
12:41:26,112 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [4] - [Thorben Janssen]
12:41:26,113 DEBUG [org.hibernate.orm.results] - Extracted JDBC value [5] - [0]
TimeZoneStorageType.AUTO
对TimeZoneStorageType.AUTO的处理取决于Hibernate的数据库特定方言。如果数据库支持列类型TIMESTAMP_WITH_TIMEZONE,Hibernate会使用*TimeZoneStorageType.NATIVE。在所有其他情况下,Hibernate使用TimeZoneStorageType.COLUMN*。
结论
尽管SQL标准定义了列类型TIMESTAMP_WITH_TIMEZONE,但并非所有数据库都支持它。这使得处理带有时区信息的时间戳变得出奇地复杂。
正如我在之前的文章中解释的,Hibernate 5支持ZonedDateTime和OffsetDateTime作为基本类型。它将时间戳规范化,并在没有时区信息的情况下进行存储,以避免数据库兼容性问题。
Hibernate 6通过引入更多的映射选项改进了这种处理方式,你现在可以选择:
- TimeZoneStorageType.NATIVE,将时间戳存储在TIMESTAMP_WITH_TIMEZONE类型的列中。
- TimeZoneStorageType.NORMALIZE将时间戳规范化为你的JDBC驱动的时区,并在没有时区信息的情况下持久保存。
- TimeZoneStorageType.NORMALIZE_UTC将时间戳规范化为UTC,并在没有时区信息的情况下持续保存。
- TimeZoneStorageType.COLUMN将没有时区信息的时间戳和所提供的时区的偏移量存储在两个独立的列中,以及
- TimeZoneStorageType.AUTO让Hibernate根据数据库的能力在TimeZoneStorageType.NATIVE和TimeZoneStorageType.COLUMN之间进行选择。