| |

Date and Time Mappings with Hibernate and JPA


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Databases support various data types to store date and time information. The most commonly used ones are:

  • DATE to save a date without time information,
  • TIME to store a time without a date, and
  • TIMESTAMP to store date and time information.

You can map all of them with JPA and Hibernate.

But you need to decide to which Java type you want to map your database column. The Java language supports a bunch of classes to represent date and time information, like:

  • The types of the Date and Time API. These are java.time.LocalDate, java.time.LocalDateTime, java.time.OffsetTime, java.time.OffsetDateTime, java.time.ZonedDateTime, java.time.Duration and all modern applications should prefer them over the old types.
  • The SQL-specific types java.sql.Date, java.sql.Time and java.sql.Timestamp,
  • And, of course, the old java.util.Date and java.util.Calendar.

Modern versions of the Jakarta Persistence standard support most of those types. In addition, Hibernate provides proprietary support for almost all of the remaining ones. I will show you how to define the different mappings in this article:

Mapping java.time classes

The classes of the Date and Time API are the most popular representations of date and time values. They distinguish between date and time information and fix the flaws of old the java.util.Date class.

Since Hibernate 5 and JPA 2.2, you can use the following classes as attribute types.

Java TypeJPAHibernateJDBC Type
java.time.LocalDatev2.2v5DATE
java.time.LocalTimev2.2v5TIME
java.time.LocalDateTimev2.2v5TIMESTAMP
java.time.OffsetTimev2.2v5TIME_WITH_TIMEZONE
java.time.OffsetDateTimev2.2v5TIMESTAMP_WITH_TIMEZONE
java.time.Instantv3.2v5TIMESTAMP
java.time.ZonedDateTimev5
v6
TIMESTAMP
TIMESTAMP_WITH_TIMEZONE
java.time.Yearv3.2v6INTEGER
java.time.Timezonev6VARCHAR
java.time.ZoneOffsetv6VARCHAR
java.time.Durationv6BIGINT

As you can see in the table, the support for the Date and Time API grew over time, and Hibernate supports a few more classes than JPA. You can easily add support for additional classes by implementing an AttributeConverter. I used it in a previous article to map an attribute of type Duration with JPA.

Depending on your Hibernate version, you should be careful when using ZonedDateTime. Hibernate 5’s handling of timezones and the mapping to a TIMESTAMP column presents a few pitfalls. Hibernate 6 gives you more control over that mapping, but you still need to understand how it works. I get into more details about that in the Working with ZonedDateTime section.

Let’s first take a look at a basic entity mapping and a simple test case using that entity.

@Entity
public class MyEntity {

	private LocalDate localDate;

	private LocalDateTime localDateTime;

	private OffsetTime offsetTime;

	private OffsetDateTime offsetDateTime;

	// Hibernate-specific - not supported by JPA

	private Duration duration;

	private Instant instant;

	private ZonedDateTime zonedDateTime;
	
	...
	
}
MyEntity e = new MyEntity();
e.setLocalDate(LocalDate.of(2019, 7, 19));
e.setLocalDateTime(LocalDateTime.of(2019, 7, 19, 15, 05, 30));
e.setOffsetTime(OffsetTime.of(15, 05, 30, 0, ZoneOffset.ofHours(+2)));
e.setOffsetDateTime(OffsetDateTime.of(2019, 7, 19, 15, 05, 30, 0, ZoneOffset.ofHours(+2)));

// Hibernate-specific - not supported by JPA
e.setDuration(Duration.ofHours(2));
e.setInstant(Instant.now());
e.setZonedDateTime(ZonedDateTime.of(2019, 7, 18, 15, 05, 30, 0, ZoneId.of("UTC-4")));

em.persist(e);

The classes of the Date and Time API clearly define if they store date and/or time information. So, the JPA specification and all implementing frameworks can map them to the correct SQL types.

11:52:26,305 DEBUG SQL:94 - insert into MyEntity (duration, instant, localDate, localDateTime, offsetDateTime, offsetTime, sqlDate, sqlTime, sqlTimestamp, utilCalendar, utilDate, zonedDateTime, id) values (?, ?, ?, ?, ?, ?, ?, ?)
11:52:26,306 TRACE BasicBinder:65 - binding parameter [1] as [BIGINT] - [PT2H]
11:52:26,307 TRACE BasicBinder:65 - binding parameter [2] as [TIMESTAMP] - [2019-07-22T09:52:26.284946300Z]
11:52:26,308 TRACE BasicBinder:65 - binding parameter [3] as [DATE] - [2019-07-19]
11:52:26,308 TRACE BasicBinder:65 - binding parameter [4] as [TIMESTAMP] - [2019-07-19T15:05:30]
11:52:26,312 TRACE BasicBinder:65 - binding parameter [5] as [TIMESTAMP] - [2019-07-19T15:05:30+02:00]
11:52:26,313 TRACE BasicBinder:65 - binding parameter [6] as [TIME] - [15:05:30+02:00]
11:52:26,324 TRACE BasicBinder:65 - binding parameter [7] as [TIMESTAMP] - [2019-07-18T15:05:30-04:00[UTC-04:00]]
11:52:26,324 TRACE BasicBinder:65 - binding parameter [8] as [BIGINT] - [1]

Working with ZonedDateTime

A ZonedDateTime object represents a date with time and timezone information, and the JDBC type TIMESTAMP_WITH_TIMEZONE seems to be the perfect match. Unfortunately, not all relational databases support that type, and Hibernate has to perform a type conversion.

As I mentioned earlier, the handling of ZonedDateTime depends on your Hibernate version:

  • Hibernate 6 supports different mapping options that give you full control over the timezone handling.
  • Hibernate 5 normalizes the timezone of your ZonedDateTime object, and that can cause problems.

Hibernate 6’s ZonedDateTime support

Hibernate 6 introduced the TimeZoneStorageType enum. It enables you to define how Hibernate shall handle the timezone information and enables you to avoid any timezone normalizations.

Defining your timezone handling

You can use those enum values to define a default timezone handling by setting the hibernate.timezone.default_storage property in your persistence.xml configuration.

<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>

You can also annotate your ZonedDateTime attribute with a @TimeZoneStorage annotation.

@Entity
public class MyEntity {

	@TimeZoneStorage(TimeZoneStorageType.NATIVE)
	private ZonedDateTime zonedDateTime;

	...
}
Hibernate’s TimeZoneStorageTypes

Here’s a quick overview of Hibernate’s different TimeZoneStorageTypes. You can find a more detailed description of each type in this article.

TimeZoneStorageTypeVersionJDBC Type
NATIVE6.0TIMESTAMP_WITH_TIMEZONE
NORMALIZE6.0TIMESTAMP
Normalizes timestamp to local or configured timezone. (Same handling as in Hibernate 5)
Default in Hibernate 6.0 and 6.1.
NORMALIZE_UTC6.0TIMESTAMP
Normalizes timestamp to UTC.
COLUMN6.0TIMESTAMP + INTEGER
Stores timestamp and timezone offset in separate columns.
AUTO6.0Depends on the database dialect:
TIMESTAMP_WITH_TIMEZONE if the database supports it,
COLUMN otherwise.
DEFAULT6.2Depends on the database dialect:
TIMESTAMP_WITH_TIMEZONE if the database supports it,
NORMALIZE_UTC otherwise.
TimeZoneStorageTypes

Hibernate 5’s ZonedDateTime support

Hibernate 5 maps a ZonedDateTime to an SQL TIMESTAMP without time zone information.

It does that by normalizing the timestamp. It converts the ZonedDateTime into the local time zone of the JVM and stores it in the database. And when it reads the TIMESTAMP, it adds the local time zone information.

MyEntity e = new MyEntity();
e.setZonedDateTime(ZonedDateTime.of(2019, 7, 18, 15, 05, 30, 0, ZoneId.of("UTC-4")));
em.persist(e);

Hibernate 5 shows the time zone information in the log message.

09:57:08,918 DEBUG SQL:92 - insert into MyEntity (zonedDateTime, id) values (?, ?)
09:57:08,959 TRACE BasicBinder:65 - binding parameter [1] as [TIMESTAMP] - [2019-07-18T15:05:30-04:00[UTC-04:00]]
09:57:08,961 TRACE BasicBinder:65 - binding parameter [2] as [BIGINT] - [1]

But you can see in the database that it converted the time zone from UTC-4 to UTC+2, which is my local time zone.

ZonedDateTime gets stored as UTC+2 without time zone information

This mapping works as long as:

  • you use a time zone without daylight saving time,
  • all instances of your application use the same time zone, and
  • you never change this time zone.

It should be obvious that this should

You can avoid these problems by configuring a time zone without daylight saving time in your persistence.xml configuration. Hibernate 5 will then use the configured time zone instead of the one used by your local JVM. I recommend using the UTC time zone.

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
            <property name="hibernate.jdbc.time_zone" value="UTC"/>
            ...
        </properties>
    </persistence-unit>
</persistence>

When you now rerun the test, you will not see any difference in the log file.

10:06:41,070 DEBUG SQL:92 - insert into MyEntity (zonedDateTime, id) values (?, ?)
10:06:41,107 TRACE BasicBinder:65 - binding parameter [1] as [TIMESTAMP] - [2019-07-18T15:05:30-04:00[UTC-04:00]]
10:06:41,108 TRACE BasicBinder:65 - binding parameter [2] as [BIGINT] - [1]

But if you look at the database table, you can see, that Hibernate 5 now converted the ZonedDateTime to the UTC time zone.

ZonedDateTime gets stored as UTC without time zone information

Mapping java.util classes

Before the release of Java 8, java.util.Date and java.util.Calendar were the most commonly used classes to represent dates with and without time information.

You can, of course, map both of them with JPA and Hibernate. But the mapping requires a few additional information. You need to define if you want to map the java.util.Date or java.util.Calendar to a column of type DATE, TIME, or TIMESTAMP.

You can do that by annotating the entity attribute with @Temporal and providing a TemporalType enum value as a value. You can choose between:

  • TemporalType.DATE to map it to a SQL DATE column
  • TemporalType.TIME to map it to a SQL TIME column
  • TemporalType.TIMESTAMP to map it to a SQL TIMESTAMP column

I’m using the @Temporal annotation in the following code snippet to map an attribute of type java.util.Date to a TIMESTAMP column and an attribute of type java.util.Calendar to a DATE column.

@Entity
public class MyEntity {

	@Temporal(TemporalType.TIMESTAMP)
	private Date utilDate;
	
	@Temporal(TemporalType.DATE)
	private Calendar utilCalendar;
	
	...

}

You can then use these attributes in the same way you use any other entity attributes.

MyEntity e = new MyEntity();
e.setUtilDate(new Date(119, 6, 18));
e.setUtilCalendar(new GregorianCalendar(2019, 6, 18));
em.persist(e);

If you activate the logging of SQL statements, you can find the following messages in your log file.

16:04:07,185 DEBUG SQL:92 - insert into MyEntity (utilCalendar, utilDate, id) values (?, ?, ?)
16:04:07,202 TRACE BasicBinder:65 - binding parameter [8] as [DATE] - [java.util.GregorianCalendar[time=1563400800000,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Berlin",offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Berlin,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2019,MONTH=6,WEEK_OF_YEAR=29,WEEK_OF_MONTH=3,DAY_OF_MONTH=18,DAY_OF_YEAR=199,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=3,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=0,ZONE_OFFSET=3600000,DST_OFFSET=3600000]]

16:04:07,207 TRACE BasicBinder:65 - binding parameter [2] as [TIMESTAMP] - [Thu Jul 18 00:00:00 CEST 2019]
16:04:07,208 TRACE BasicBinder:65 - binding parameter [3] as [BIGINT] - [1]

The 2nd message about the binding of the GregorianCalendar might surprise you. That’s Hibernate’s very complicated way to show you which Calendar object gets bound to a parameter of type DATE. But don’t worry, if you take a look at the database, you can see that Hibernate wrote the date to a column of type DATE.

java.util.Date and java.util.Calendar got persisted as a TIMESTAMP and a DATE

Mapping java.sql classes

Mapping the java.sql classes Date, Time, and Timestamp is simple because the classes match the SQL data types. That enables your persistence provider, e.g., Hibernate, to identify the mapping automatically.

So, without providing any additional annotations:

  • java.sql.Date gets mapped to SQL DATE,
  • java.sql.TIME gets mapped to SQL TIME and
  • java.sql.TIMESTAMP gets mapped to SQL TIMESTAMP.
@Entity
public class MyEntity {

	private java.sql.Date sqlDate;
	
	private Time sqlTime;
	
	private Timestamp sqlTimestamp;
	
	...

}

You can then use these attributes in your business code to store date and time information in your database.

MyEntity e = new MyEntity();
e.setSqlDate(new java.sql.Date(119, 6, 18));
e.setSqlTime(new Time(15, 05, 30));
e.setSqlTimestamp(new Timestamp(119, 6, 18, 15, 05, 30, 0));
em.persist(e);

And after you activated the logging of SQL statements, you can see that Hibernate maps the entity attributes to the corresponding SQL types.

06:33:09,139 DEBUG SQL:92 - insert into MyEntity (sqlDate, sqlTime, sqlTimestamp, id) values (?, ?, ?, ?)
06:33:09,147 TRACE BasicBinder:65 - binding parameter [1] as [DATE] - [2019-07-18]
06:33:09,147 TRACE BasicBinder:65 - binding parameter [2] as [TIME] - [15:05:30]
06:33:09,147 TRACE BasicBinder:65 - binding parameter [3] as [TIMESTAMP] - [2019-07-18 15:05:30.0]
06:33:09,154 TRACE BasicBinder:65 - binding parameter [4] as [BIGINT] - [1]

Conclusion

JPA and Hibernate can map database columns of type DATE, TIME and TIMESTAMP to various Java classes. You can map them to:

  • java.util.Date and java.util.Calendar
  • java.sql.Date, java.sql.Time and java.sql.Timestamp
  • java.time.LocalDate, java.time.LocalDateTime, java.time.OffsetTime, java.time.OffsetDateTime, java.time.ZonedDateTime, java.time.Duration

You just need to decide which Java type you want to use in your code. I recommend using the classes in the java.time package. They are part of the Date and Time API, which was introduced in Java 8. These classes are a lot easier to use in your mapping and your business code.

One Comment

  1. excellent tutorial on a very error-prone topic

Comments are closed.