Using Java 8 Dates with Spring JPA and PostgreSQL for Custom Queries

October 5, 2018

Storing dates in databases was traditionally done with java.sql.Date and java.sql.Timestamp. Since we would like to write modern applications, with Java 8 or 11, it is preferable to use an OffsetDateTime or a LocalDateTime. Spring Data JPA, for example, is mapping those fields automatically to the correct database field. There are still some challenges in combination with PostgreSQL.

Behavior of the PostgreSQL Driver

The PostgreSQL driver tries to figure out the type of the parameters to tell those parameters directly to the PostgreSQL Server. This is necessary that the PostgreSQL server is able to compare fields. In case of a java.sql.Timestamp the PostgreSQL driver is just not able to do it since there are two matching fields for PostgreSQL. On the one side, there is the timestamp and on the other side the timestamptz with timezone information. The result is, that the PostgreSQL driver will just match it to Oid.UNSPECIFIED. This behavior is most of the time not an issue since the PostgreSQL server is able to detect the type. There is a detailed description of this issue in the PostgreSQL driver class PgPreparedStatement

Entering the JPA Date Type Fight

In case you would like to provide an optional date field to a JPA query, you are facing an interesting problem with the following JPQL statement:

select p from Persons p where (:createdAt is null or p.createdAt > :createdAt)

This will fail with the following exception:

org.postgresql.util.PSQLException: ERROR: could not determine data type of parameter $1

Most of the results are that you should cast it to a timestamp with ::timestamp or ::timestamptz. Actually, those are not working with JPQL, since there you need to use the JPQL syntax for the cast: cast(:createdAt as timestamp). The next obvious solution would be to add casts to both of them:

select p from Persons p where (cast(:createdAt as timestamp) is null or p.createdAt > cast(:createdAt as timestamp))

This will work in case you are providing a date, but it will fail in case :createdAt is null:

org.postgresql.util.PSQLException: ERROR: cannot cast type bytea to timestamp without time zone
  Position: 378

Taming the JPA Date Type Biest

The not really obvious solution is to cast only the types on this places where PostgreSQL on the server is not able to automatically detect the cast:

select p from Persons p where (cast(:createdAt as timestamp) is null or p.createdAt > :createdAt)

For the is null it's not obvious which type it is, but for the date comparison, it can just detect the type by using the type on the left side.

Conclusion

There are a lot of challenges with JPA during handling date times. The issues mentioned here are related to the specific implementation of the PostgreSQL driver. In the unit tests with an embedded H2 database, we were not able to reproduce those issues. You might experience similar issues with JDBC.

About the author: Valentin Zickner

Is working since 2016 at mimacom as a Software Engineering. He has a lot of experience with cloud technologies, in addition he is specialized to Spring and Elasticsearch.