Skip navigation

montanajava

1 post

Using the Java 8 Date Time Classes with JPA!

With the Java 8 SE release, developers get a splendid new best-in-class Date-Time API. Wouldn't it be nice if you could use it with JPA? Not so fast. JPA and for that matter JDBC know nothing about the new classes, and if you use them in your entities, JPA will map them to BLOBs in your database by default. This happens in DDL or database creation, in queries, and in inserts and updates. Take a simple entity as an example:

@Entity
public class Trip {
    @Id long id;
    LocalDateTime departure;
}

The persistence of the departure attribute will work perfectly, if you like saving DateTimes as BLOBs that is. A BLOB for a datetime is hardly the mapping anyone would want, adversely affecting your indexing and querying options, not to mention being an inefficient choice for storage. If you are taking advantage of either the database or the DDL file generation features of JPA 2.1, the above Entity will also result in a BLOB departure field being created in the Trip table, directly or indirectly. What most people will want is a sensible mapping to the Date, Time, and Timestamp types of SQL. This can be achieved by creating custom converters under JPA 2.1 that use old-school classes from the java.sql package which in turn are supported by JPA and JDBC. Here is an example for LocalDate:

@Converter(autoApply = true)
public class LocalDatePersistenceConverter implements
    AttributeConverter {
    @Override
    public java.sql.Date convertToDatabaseColumn(LocalDate entityValue) {
        return java.sql.Date.valueOf(entityValue);
    }

    @Override
    public LocalDate convertToEntityAttribute(java.sql.Date databaseValue) {
        return databaseValue.toLocalDate();
    }
}

Easy as pie. JPA 2.1  gives you a portable way of fixing our not-quite-up-to-snuff JDBC/JPA specification dilemma, simply by providing you with a standard way to create a custom converter that implements javax.persistence.AttributeConverter. This interface provides two methods that define the mappings: The first one is from a JDBC supported class when you are reading from the database convertToEntityAttribute; the other one is used to map to a JDBC supported class when you are persisting to the database convertToDatabaseColumn. The usual suspects for such mappings will be java.sql.Date, java.sql.Time, and java.sql.Timestamp and even the old stalwart java.lang.String. In the case of LocalDateTime, the converter looks like this:

@Converter(autoApply = true)
public class LocalDateTimePersistenceConverter implements
    AttributeConverter {
    @Override
    public java.sql.Timestamp convertToDatabaseColumn(LocalDateTime entityValue) {
        return Timestamp.valueOf(entityValue);
    }

    @Override
    public LocalDateTime convertToEntityAttribute(java.sql.Timestamp databaseValue) {
        return databaseValue.toLocalDateTime();
    }
}

By mapping LocalDateTime to java.sql.Timestamp, we get an automatic mapping to the SQL type we wish to have. Notice the @Converter annotation which has an autoapply parameter, here set to true. That means that you can now use LocalDateTime in any of your entities without requiring further annotation. If this is not what you want, set it to false, and then in your entity annotate your attribute like this:

@Convert(converter = LocalTimePersistenceConverter.class)
LocalTime departure;

JDBC has no concept of persisting timezone information with dates and timestamps. For the LocalDate and LocalDateTime classes, that is a good semantic fit. If you do wish to store java.time classes that include  timezone information, mapping these to a String will provide for an easy, portable solution. See the following example for OffsetDateTime:

@Converter(autoApply = true)
public class OffsetDateTimePersistenceConverter implements
    AttributeConverter {

    /**
    * @return a value as a String such as 2014-12-03T10:15:30+01:00
    * @see OffsetDateTime#toString()
    */
    @Override
    public String convertToDatabaseColumn(OffsetDateTime entityValue) {
        return Objects.toString(entityValue, null);
    }

    @Override
    public OffsetDateTime convertToEntityAttribute(String databaseValue) {
        return OffsetDateTime.parse(databaseValue);
    }
}

It keeps getting better! Java 8 now has extremely handy Period and Duration classes (anyone out there ever have to deal with schedules or timetables?), as well as sundry other classes, for which you may find the need persist instances. These too can be easily persisted as Strings with simple converters. Here is a converter for Period using existing JDBC String mapping to do the dirty work:

@Converter(autoApply = true)
public class PeriodPersistenceConverter implements AttributeConverter {

    /**
    * @return an ISO-8601 representation of this duration.
    * @see Period#toString()
    */
    @Override
    public String convertToDatabaseColumn(Period entityValue) {
        return Objects.toString(entityValue, null);
    }

    @Override
    public Period convertToEntityAttribute(String databaseValue) {
        return Period.parse(databaseValue);
    }
}

The JPA persistence unit needs to know about the converters. This can be done by adding the converters to the persistence.xml. An excerpt from the persistence.xml for the unit tests of the converters looks like this:

<?xml version="1.0" encoding="UTF-8"?> <persistence version="1.0"             xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="java8DateTimeTestPersistenceUnit"                     transaction-type="RESOURCE_LOCAL">     <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>     <class>org.parola.domain.Book</class>     <class>org.parola.domain.Appointment</class>     <class>org.parola.domain.SpaceTravel</class>     ...     <class>org.parola.util.date.LocalDatePersistenceConverter</class>     <class>org.parola.util.date.LocalTimePersistenceConverter</class>     <class>org.parola.util.date.LocalDateTimePersistenceConverter</class>     ...     <properties>         <property name="eclipselink.logging.level" value="INFO" />         <property name="eclipselink.target-database" value="DERBY" />         <property name="javax.persistence.jdbc.driver"                 value="org.apache.derby.jdbc.EmbeddedDriver" />         <property name="javax.persistence.jdbc.url"                 value="jdbc:derby:memory:java8DateTimeTest;create=true" />         <property name="javax.persistence.jdbc.user" value="" />         <property name="javax.persistence.jdbc.password" value="" />         <property name="javax.persistence.schema-generation.database.action"                 value="drop-and-create"/>     </properties> </persistence-unit> </persistence>

Some databases have custom Java types for storing timezone data along with Date/Time information, DB2 as of vs. 10 and Oracle being two examples that provide that support for zoned timestamps. If you have such a database, you can still make use of a custom JPA 2.1 converter and stay in the world of Java 8 datetime goodness and not have to use non-standard vendor-provided classes in your code.

For other databases, or in the event you do not wish to use the proprietary feature of your database, an attribute of type ZonedDateTime, using the appropriate converter, will be stored in your database in a human-readable form, and will be sortable in accordance with ISO 8601 norms, albeit not amenable to Date/Time math within SQL, although your databases should have built-in from-String converter functions. You can establish constraints in the database if you want further control over your data integrity, of potential importance for databases additionally serving non-Java 8 clients. Database functions come to mind as a solution if you need native types in the SQL world, while retaining VARCHAR storage. Any deeper mappings, such as to multiple columns separating out the timezone information, exceed what is foreseen in the current specification for converters.

Resources

The project itself is located at JPAAttributeConverters on BitBucket with a commercially-friendly license. You will need Maven to build the project. The project has extensive unit tests and to that end includes several highly minimalistic domain classes. The tests use an in-memory Derby database and a JPA 2.1-compliant driver. Be sure to test against your database, easily done by modifying the persistence.xml. You can check out the code using Mercurial: hg clone https://montanajava@bitbucket.org/montanajava/jpaattributeconverters//montanajava@bitbucket.org/montanajava/jpaattributeconverters

Have fun with the new Java 8 Date and Time classes! We say good riddance bid a fond farewell to the venerable java.util and java.sql Date/Time classes. As soon as you can use Java 8 in your environment, there is no reason to wait for updates to the JPA and JDBC specifications to be able to beneifit from these classes. If support does become available within a future standard, modification to your code should consist of removing some lines from the persistence.xml and deleting some classes.

Filter Blog

By date: