|

How To Map The Date And Time API with JPA 2.2


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.


As expected, the recent release of the JPA 2.2 specification introduced official support for some of the classes of the Date and Time API. Before that, you had to rely on proprietary features, like the ones introduced in Hibernate 5, or you had to provide an AttributeConverter to implement the mapping.

Let’s take a closer look at JPA’s new support for the Date and Time API and how you can use it in your project.

Get The JPA 2.2 API And Reference Implementation

The following maven dependency adds the API jar of the JPA 2.2 specification to your project.

<dependency>
  <groupId>javax.persistence</groupId>
  <artifactId>javax.persistence-api</artifactId>
  <version>2.2</version>
</dependency>

You can implement your application with the API jar but you, obviously, need an implementation at runtime. You can use EclipseLink 2.7 or Hibernate 5.2.

<dependency>
  <groupId>org.eclipse.persistence</groupId>
  <artifactId>eclipselink</artifactId>
  <version>2.7.0-RC3</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>5.2.10.Final</version>
</dependency>

That’s all you have to do to add the JPA 2.2 API and an implementation to your project.

Limited Support For The Date And Time API In JPA 2.2

The JPA 2.2 specification maps only the classes of the Date and Time API for which the appendix section B4 of the JDBC 4.2 specification defines a mapping. These are:

Java TypeJDBC Type
java.time.LocalDateDATE
java.time.LocalTimeTIME
java.time.LocalDateTimeTIMESTAMP
java.time.OffsetTimeTIME_WITH_TIMEZONE
java.time.OffsetDateTimeTIMESTAMP_WITH_TIMEZONE

As expected, this list includes LocalDate, LocalDateTime and LocalTime. And it also defines a mapping for OffsetTime and OffsetDatetime. These mappings include the offset of the local timezone to UTC.

But JPA doesn’t support ZonedDateTime which stores additional timezone information, like daylight saving time.

Supported As Basic Types

JPA 2.2 supports the listed classes as basic types and you don’t need to provide any additional mapping information. That is one of the benefits of the Date and Time API. It uses separate classes to model date or date and time information. So, your JPA implementation doesn’t need any additional information to map the Java object to the correct JDBC type.

The following code snippet shows a simple example of such a mapping. The entity uses the AUTO strategy to generate the primary key value and persists attributes of type LocalDate, LocalTime, LocalDateTime, OffsetTime and OffsetDateTime.

@Entity
public class MyEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = "id", updatable = false, nullable = false)
	private Long id;

	private LocalDate date;

	private LocalTime time;

	private LocalDateTime dateTime;

	private OffsetTime offsetTime;

	private OffsetDateTime offsetDateTime;
	
	...
}

You can use these attributes in the same way as you use any other entity attribute and your persistence provider includes them in the generated SQL statements.

MyEntity e = new MyEntity();
e.setDate(LocalDate.now());
e.setTime(LocalTime.now());
e.setDateTime(LocalDateTime.now());
e.setOffsetTime(OffsetTime.now());
e.setOffsetDateTime(OffsetDateTime.now());

em.persist(e);
[EL Fine]: sql: 2017-08-25 13:27:55.128--ClientSession(1709225221)--Connection(1763750076)--INSERT INTO MYENTITY (id, DATE, DATETIME, OFFSETDATETIME, OFFSETTIME, TIME) VALUES (?, ?, ?, ?, ?, ?)
	bind => [1, 2017-08-25, 2017-08-25T13:27:55.091, 2017-08-25T13:27:55.092+02:00, 13:27:55.091+02:00, 13:27:55.091]

And you can also use them in a JPQL or CriteriaQuery. The following example selects all records mapped by the MyEntity class which value of the date attribute is equal to the current day.

TypedQuery<MyEntity> q = em.createQuery("SELECT m FROM MyEntity m WHERE m.date = :date", MyEntity.class);
q.setParameter("date", LocalDate.now());
MyEntity e2 = q.getSingleResult();
[EL Fine]: sql: 2017-08-25 13:27:55.881--ServerSession(1636824514)--Connection(1763750076)--SELECT id, DATE, DATETIME, OFFSETDATETIME, OFFSETTIME, TIME FROM MYENTITY WHERE (DATE = ?)
	bind => [2017-08-25]

Extended Support in Hibernate 5

Hibernate supports the classes of the Date and Time API since version 5 as basic types. And it not only supports the 5 mappings defined in the JPA 2.2 specification. It also maps Duration, Instant and ZonedDateTime objects. Here is a full list of the supported Java types and the JDBC types to which Hibernate maps them.

Java TypeJDBC Type
java.time.LocalDateDATE
java.time.LocalTimeTIME
java.time.LocalDateTimeTIMESTAMP
java.time.OffsetTimeTIME_WITH_TIMEZONE
java.time.OffsetDateTimeTIMESTAMP_WITH_TIMEZONE
java.time.DurationBIGINT
java.time.InstantTIMESTAMP
java.time.ZonedDateTimeTIMESTAMP

The mapping of Duration and Instant are pretty obvious. But the mapping of ZonedDateTime requires a more detailed explanation. Hibernate maps it to a JDBC TIMESTAMP without timezone information.

Persisting A ZonedDateTime

So, what happens when you persist a ZonedDateTime with Hibernate? And why does it not store the timezone information?

Hibernate converts the ZonedDateTime to the local timezone and stores it in the database without timezone information.

Let’s take a look at an example. My local timezone is Europe/Berlin (UTC+2) and at the beginning of the test case, I’m getting the current time in UTC and then persist that ZonedDateTime object with Hibernate.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
log.info("Now in UTC: "+now);

MyEntity e = new MyEntity();
e.setDate(LocalDate.now());
e.setDateTime(LocalDateTime.now());
e.setZonedDateTime(now);
em.persist(e);

em.getTransaction().commit();
em.close();

As you can see in the log message, the entity attribute is still a ZonedDateTime with the timezone UTC, when Hibernate writes the bind parameters to the log file.

17:32:46,867 INFO TestHibernate5Date:45 - Now in UTC: 2017-08-25T15:32:46.866Z[UTC]
17:32:46,958 DEBUG SQL:92 - insert into MyEntity (date, dateTime, zonedDateTime, id) values (?, ?, ?, ?)
17:32:46,963 TRACE BasicBinder:65 - binding parameter [1] as [DATE] - [2017-08-25]
17:32:46,964 TRACE BasicBinder:65 - binding parameter [2] as [TIMESTAMP] - [2017-08-25T17:32:46.883]
17:32:46,966 TRACE BasicBinder:65 - binding parameter [3] as [TIMESTAMP] - [2017-08-25T15:32:46.866Z[UTC]]
17:32:46,967 TRACE BasicBinder:65 - binding parameter [4] as [BIGINT] - [1]

But when you take a look at the record in the database, you can see that Hibernate stored 17:32:46.866 instead of 15:32:46.866 in the database. It converted the ZonedDateTime object to my local timezone.

This approach works fine as long as all instances of your application use the same local timezone. But you will run into problems as soon as you change the timezone setting or your cluster gets out of sync.

And it causes problems if your timezone uses daylight saving time. Hibernate converts the ZonedDateTime to a java.sql.Timestamp, which uses an ambiguate mapping when transitioning between standard and daylight saving time.

It’s better to tell Hibernate which timezone it shall use and set it to a timezone without daylight saving time, e.g. UTC. You can do that with the configuration parameter hibernate.jdbc.time_zone, which you can set it in the persistence.xml file or on the SessionFactory.

<persistence>
  <persistence-unit name="my-persistence-unit">
  <description>Thougths on Java</description>
  <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
  <exclude-unlisted-classes>false</exclude-unlisted-classes>
  <properties>
			<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
			<property name="hibernate.jdbc.time_zone" value="UTC"/>

			<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
			<property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:5432/test" />
			<property name="javax.persistence.jdbc.user" value="postgres" />
			<property name="javax.persistence.jdbc.password" value="postgres" />
			
			<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
  </properties>
  </persistence-unit>
</persistence>

When you run the same test case with this configuration, Hibernate use the UTC timezone to store the LocalDateTime and the ZonedDateTime in the database.

Summary

Since version 2.2, JPA supports LocalDate, LocalDateTime, LocalTime, OffsetTime and OffsetDateTime as basic types. These classes provide all information that your persistence provider needs to map them to the correct JDBC type. So, in contrast to the old java.util.Date, you don’t need to provide any additional annotations to define the mapping.

In addition to the mapping defined by the JPA specification, you can also persist Duration, Instant and ZonedDateTime objects with Hibernate 5.2.

But you should be careful when using a ZonedDateTime. Hibernate converts it into the local timezone of your JVM and doesn’t store any timezone information in the database. If you want to use it, you should define a JDBC timezone with Hibernate’s hibernate.jdbc.time_zone configuration parameter. That makes sure that all instances of your application use the same timezone and that a change of the local timezone of your JVM doesn’t affect your data.

7 Comments

  1. Avatar photo Thorben Janssen says:

    Hi Leo,

    You could add another attribute to your entity that stores the name of the current timezone…

    Regards,
    Thorben

  2. Hi, have you actually tested this with Eclipselink? I’m using version 2.7.1 and getting exception:
    “The type [class java.time.LocalDateTime] for the attribute [xy] on the entity class [class Xy] is not a valid type for a temporal mapping. The attribute must be defined as java.util.Date or java.util.Calendar.”

    While debugging Eclipselink code, I stumbled upon this method that is called before Eclipselink throws that exception (in class org.eclipse.persistence.internal.jpa.metadata.converters.TemporalMetadata):

    public static boolean isValidTemporalType(MetadataClass cls) {
    return cls.equals(Date.class) || cls.equals(Calendar.class) || cls.equals(GregorianCalendar.class);
    }

    This seems very strange to me, as it clearly says on Eclipselink web page that version 2.7 has support for Java Persistence (JPA) 2.2 – JSR 338.

    Any thoughts?

    1. Just to answer to my own question. Attributes that are using LocalDate, LocalDateTime and LocalTime types must not be annotated with @Temporal, otherwise the exception will be thrown. @Temporal annotation is only needed when using old Date types.

      1. Avatar photo Thorben Janssen says:

        That’s right. You can’t annotate the classes of the Date and Time API with @Temporal.

  3. Hi.

    Using your code until Entity class with mysql, the types creted with spring.jpa.hibernate.ddl-auto=create-drop, the datatypes of:
    date
    dateTime
    etc….
    is always TINYBLOB,

    is that correct?

    I think it should be, TIME, TIMESTAMP

    These are my imports:
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    import java.time.OffsetDateTime;
    import java.time.OffsetTime;

    Thanks for your postsss.

    1. Avatar photo Thorben Janssen says:

      It looks like you’re using an old Hibernate version which doesn’t support the Date and Time API. Which Spring Data and Hibernate versions do you use?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.