This tutorial shows you how to use @TimeZoneStorage
and @TimeZoneColumn
introduced in Hibernate 6.
If you need to store a time instant in the database, you may need to store the time zone information as well. It can be stored in the same column as the time instant if the database supports it. Another alternative is to store it in another column. There are other options as well, such as normalizing the value to a specific time zone or to UTC.
Hibernate 6 introduced annotations that allow us to specify how time zone information should be stored in the database. The annotations are @TimeZoneStorage
and @TimeZoneColumn
. With those annotations, we delegate the responsibility of storing time zone information to Hibernate.
Using @TimeZoneStorage
Annotation
In Java, a time instant that contains time zone information is usually represented using ZonedDateTime
or OffsetDateTime
. With Hibernate, we can also use those types to define entity columns.
By default, Hibernate 6 uses the value of hibernate.timezone.default_storage
property to determine the default strategy for storing the timezone information. If you don't set it in the property, the default value is DEFAULT
. So, if you want to change the default storage type for all ZonedDateTime
and OffsetDateTime
fields, you have to set it in the property. The meaning of the DEFAULT
value will be explained later along with other supported types.
To specify how the time zone information of a column should be persisted, you can add a @TimeZoneStorage
annotation to the field. In the annotation, pass a TimeZoneStorageType
enum value to define the storage type to be used. The enum has some values which include NATIVE
, NORMALIZE
, NORMALIZE_UTC
, COLUMN
, AUTO
, and DEFAULT
. I'm going to explain those values one by one.
Below is an example of an entity that uses the annotation. It has a field of type ZonedDateTime
and another field of type OffsetDateTime
. Both fields are annotated with @TimeZoneStorage
.
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
@GenericGenerator(name = "UUID", type = UuidGenerator.class)
private UUID id;
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private ZonedDateTime orderedAt;
@TimeZoneStorage(TimeZoneStorageType.NATIVE)
private OffsetDateTime issuedAt;
}
We are going to use the simple code below for persisting and retrieving records of the entity above. The times are hard-coded with the zone set to 'Asia/Jakarta' (GMT+7).
ZonedDateTime currentTime = ZonedDateTime.of(2023, 6, 11, 15, 0, 0, 0, ZoneId.of("Asia/Jakarta"));
OffsetDateTime currentTime2 = OffsetDateTime.of(2023, 6, 11, 15, 0, 0, 0, ZoneOffset.of("+07:00"));
Order order = Order.builder()
.orderedAt(currentTime)
.issuedAt(currentTime2)
.build();
Order savedOrder = this.orderRepository.save(order);
Order retrievedOrder = this.orderRepository.findById(savedOrder.getId())
.orElse(null);
TimeZoneStorageType.NATIVE
With NATIVE
strategy, the value is stored in a column with time zone (SqlType.TIMESTAMP_WITH_TIMEZONE
). Therefore, the database you use must support that data type and you also need to create the column with the correct type. Otherwise, you may get an error or a wrong value.
Log:
insert into orders (id,ordered_at,issued_at) values (?,?,?)
binding parameter [1] as [UUID] - [0f2f73c0-3d37-4a95-97f2-7ee3e2e27c90]
binding parameter [2] as [TIMESTAMP_WITH_TIMEZONE] - [2023-06-11T15:00+07:00[Asia/Jakarta]]
binding parameter [3] as [TIMESTAMP_WITH_TIMEZONE] - [2023-06-11T15:00+07:00]
Database values:
ordered_at: 2023-06-11 15:00:00+07
issued_at: 2023-06-11 15:00:00+07
Retrieved values:
orderedAt: 2023-06-11T08:00Z
issued_at: 2023-06-11T08:00Z
TimeZoneStorageType.NORMALIZE
This storage type normalizes the value to a specific time zone based on hibernate.jdbc.time_zone
property. If the property is not set, Hibernate will use the JVM default time zone. When the value is retrieved from the database, the zone (for ZonedDateTime
) or the offset (for OffsetDateTime
) will be set to the JVM default time zone. If you use this type, the column that stores the timestamp should not have the timezone
Log:
insert into orders (id,ordered_at,issued_at) values (?,?,?)
binding parameter [1] as [UUID] - [0f2f73c0-3d37-4a95-97f2-7ee3e2e27c90]
binding parameter [2] as [TIMESTAMP] - [2023-06-11T15:00+07:00[Asia/Jakarta]]
binding parameter [3] as [TIMESTAMP] - [2023-06-11T15:00+07:00]
Database values:
ordered_at: 2023-06-11 15:00:00
issued_at: 2023-06-11 15:00:00
Retrieved values:
orderedAt: 2023-06-11T15:00+07:00[Indian/Christmas]
issued_at: 2023-06-11T15:00+07:00
TimeZoneStorageType.NORMALIZE_UTC
NORMALIZE_UTC
is similar to NORMALIZE
, which doesn't preserve the time zone. The difference is the timestamp is normalized to UTC. When the value is retrieved from the database, the zone (for ZonedDateTime
) or the offset (for OffsetDateTime
) will be set to UTC.
Warning: For NORMALIZE_UTC
, currently there is a potential issue that causes the value to be not stored as UTC value in the database.
Log:
insert into orders (id,ordered_at,issued_at) values (?,?,?,?,?,?)
binding parameter [1] as [UUID] - [0f2f73c0-3d37-4a95-97f2-7ee3e2e27c90]
binding parameter [2] as [TIMESTAMP_UTC] - [2023-06-11T15:00+07:00[Asia/Jakarta]]
binding parameter [3] as [TIMESTAMP_UTC] - [2023-06-11T15:00+07:00]
(Expected) Database values:
ordered_at: 2023-06-11 08:00:00 // because of the current issue, stored as 2023-06-11 15:00:00
issued_at: 2023-06-11 08:00:00 // because of the current issue, stored as 2023-06-11 15:00:00
(Expected) Retrieved values:
orderedAt: 2023-06-11T08:00Z // because of the current issue, retrieved as 2023-06-11T15:00Z
issued_at: 2023-06-11T08:00Z // because of the current issue, retrieved as 2023-06-11T15:00Z
TimeZoneStorageType.COLUMN
The COLUMN
strategy stores the timezone information in a separate column of integer type. By default, the name for the timezone column is the column name that stores the timestamp, added with _tz
suffix. That means the value of the timestamp column is normalized to UTC.
Warning: For COLUMN
, currently there is a potential issue that causes the value to be not stored as UTC value in the database.
Log:
insert into orders (id,ordered_at,ordered_at_tz,issued_at,issued_at_tz) values (?,?,?,?,?)
binding parameter [1] as [UUID] - [0f2f73c0-3d37-4a95-97f2-7ee3e2e27c90]
binding parameter [2] as [TIMESTAMP_UTC] - [2023-06-11T08:00:00Z]
binding parameter [3] as [INTEGER] - [+07:00]
binding parameter [4] as [TIMESTAMP_UTC] - [2023-06-11T08:00:00Z]
binding parameter [5] as [INTEGER] - [+07:00]
(Expected) Database values:
ordered_at: 2023-06-11 08:00:00 // because of the current issue, stored as 2023-06-11 15:00:00
ordered_at_tz: 25200
issued_at: 2023-06-11 08:00:00 // because of the current issue, stored as 2023-06-11 15:00:00
issued_at_tz: 25200
(Expected) Retrieved values:
orderedAt: 2023-06-11T15:00+07:00, because of the current issue, retrieved as 2023-06-11T22:00+07:00
issued_at: 2023-06-11T15:00+07:00, because of the current issue, retrieved as 2023-06-11T22:00+07:00
TimeZoneStorageType.AUTO
If the type is set to AUTO
, it will use NATIVE
if Dialect#getTimeZoneSupport()
is set to NATIVE. Otherwise, it will use the COLUMN
strategy. To change the value returned by Dialect#getTimeZoneSupport()
, you can create a custom Dialect and override the method.
public class MyCustomDialect extends PostgreSQLDialect {
@Override
public TimeZoneSupport getTimeZoneSupport() {
return TimeZoneSupport.NATIVE;
}
}
TimeZoneStorageType.DEFAULT
If the type is set to DEFAULT
, it will use NATIVE
if Dialect#getTimeZoneSupport()
is set to NATIVE. Otherwise, it will use the NORMALIZE_UTC
strategy.
Using @TimeZoneColumn
Annotation
The @TimeZoneColumn
annotation can be used to specify a custom name for the column that stores the timezone information. By default, the name is the same as the name of the timestamp column, added with _tz
suffix. This annotation can only be used if the TimeZoneStorageType
is COLUMN
or AUTO
.
If you use it with an incompatible storage type, you'll get the following exception.
Caused by: java.lang.IllegalStateException: @TimeZoneColumn can not be used in conjunction with @TimeZoneStorage( NATIVE ) with attribute com.woolha.hibernate6.model.Order.processedAt
Summary
The @TimeZoneStorage
makes it easier to specify how a time instant with time zone information should be persisted to the database. With that annotation, the process of converting a time instant to a database column value becomes the responsibility of Hibernate. If the time zone information is stored in another column, you can use @TimeZoneColumn
to define the column name.