Using java.time.LocalDateTime, LocalDate and LocalTime in Hibernate

Java8 introduces new date and time types which are better than java.lang.Date. Unfortunately though, the latest Hibernate (4.2.7.SP1 at the time of writing) does not support these types.

Here are some classes you can use to serialize these:

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.usertype.EnhancedUserType;
 
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
 
public class LocalDateTimeUserType implements EnhancedUserType, Serializable {
 
    private static final int[] SQL_TYPES = new int[]{Types.TIMESTAMP};
 
    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }
 
    @Override
    public Class returnedClass() {
        return LocalDateTime.class;
    }
 
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) {
            return true;
        }
        if (x == null || y == null) {
            return false;
        }
        LocalDateTime dtx = (LocalDateTime) x;
        LocalDateTime dty = (LocalDateTime) y;
        return dtx.equals(dty);
    }
 
    @Override
    public int hashCode(Object object) throws HibernateException {
        return object.hashCode();
    }
 
 
    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        Object timestamp = StandardBasicTypes.TIMESTAMP.nullSafeGet(resultSet, names, session, owner);
        if (timestamp == null) {
            return null;
        }
        Date ts = (Date) timestamp;
        Instant instant = Instant.ofEpochMilli(ts.getTime());
        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
    }
 
    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        if (value == null) {
            StandardBasicTypes.TIMESTAMP.nullSafeSet(preparedStatement, null, index, session);
        } else {
            LocalDateTime ldt = ((LocalDateTime) value);
            Instant instant = ldt.atZone(ZoneId.systemDefault()).toInstant();
            Date timestamp = Date.from(instant);
            StandardBasicTypes.TIMESTAMP.nullSafeSet(preparedStatement, timestamp, index, session);
        }
    }
 
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
 
    @Override
    public boolean isMutable() {
        return false;
    }
 
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }
 
    @Override
    public Object assemble(Serializable cached, Object value) throws HibernateException {
        return cached;
    }
 
    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
 
    @Override
    public String objectToSQLString(Object object) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public String toXMLString(Object object) {
        return object.toString();
    }
 
    @Override
    public Object fromXMLString(String string) {
        return LocalDateTime.parse(string);
    }
 
}
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.usertype.EnhancedUserType;
 
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
 
public class LocalDateUserType implements EnhancedUserType, Serializable {
 
    private static final int[] SQL_TYPES = new int[]{Types.DATE};
 
    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }
 
    @Override
    public Class returnedClass() {
        return LocalDate.class;
    }
 
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) {
            return true;
        }
        if (x == null || y == null) {
            return false;
        }
        LocalDate dtx = (LocalDate) x;
        LocalDate dty = (LocalDate) y;
        return dtx.equals(dty);
    }
 
    @Override
    public int hashCode(Object object) throws HibernateException {
        return object.hashCode();
    }
 
 
    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        Object timestamp = StandardBasicTypes.DATE.nullSafeGet(resultSet, names, session, owner);
        if (timestamp == null) {
            return null;
        }
        Date date = (Date) timestamp;
        Instant instant = Instant.ofEpochMilli(date.getTime());
        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalDate();
    }
 
    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        if (value == null) {
            StandardBasicTypes.DATE.nullSafeSet(preparedStatement, null, index, session);
        } else {
            LocalDate ld = ((LocalDate) value);
            Instant instant = ld.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
            Date time = Date.from(instant);
            StandardBasicTypes.DATE.nullSafeSet(preparedStatement, time, index, session);
        }
    }
 
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
 
    @Override
    public boolean isMutable() {
        return false;
    }
 
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }
 
    @Override
    public Object assemble(Serializable cached, Object value) throws HibernateException {
        return cached;
    }
 
    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
 
    @Override
    public String objectToSQLString(Object object) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public String toXMLString(Object object) {
        return object.toString();
    }
 
    @Override
    public Object fromXMLString(String string) {
        return LocalDate.parse(string);
    }
 
}
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.usertype.EnhancedUserType;
 
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
 
public class LocalTimeUserType implements EnhancedUserType, Serializable {
 
    private static final int[] SQL_TYPES = new int[]{Types.TIME};
    private static final int A_YEAR = 2000;
    private static final int A_MONTH = 1;
    private static final int A_DAY = 1;
 
    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }
 
    @Override
    public Class returnedClass() {
        return LocalTime.class;
    }
 
    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) {
            return true;
        }
        if (x == null || y == null) {
            return false;
        }
        LocalTime dtx = (LocalTime) x;
        LocalTime dty = (LocalTime) y;
        return dtx.equals(dty);
    }
 
    @Override
    public int hashCode(Object object) throws HibernateException {
        return object.hashCode();
    }
 
 
    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        Object timestamp = StandardBasicTypes.TIME.nullSafeGet(resultSet, names, session, owner);
        if (timestamp == null) {
            return null;
        }
        Date time = (Date) timestamp;
        Instant instant = Instant.ofEpochMilli(time.getTime());
        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalTime();
    }
 
    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        if (value == null) {
            StandardBasicTypes.TIME.nullSafeSet(preparedStatement, null, index, session);
        } else {
            LocalTime lt = ((LocalTime) value);
            Instant instant = lt.atDate(LocalDate.of(A_YEAR, A_MONTH, A_DAY)).
                    atZone(ZoneId.systemDefault()).toInstant();
            Date time = Date.from(instant);
            StandardBasicTypes.TIME.nullSafeSet(preparedStatement, time, index, session);
        }
    }
 
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
 
    @Override
    public boolean isMutable() {
        return false;
    }
 
    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }
 
    @Override
    public Object assemble(Serializable cached, Object value) throws HibernateException {
        return cached;
    }
 
    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
 
    @Override
    public String objectToSQLString(Object object) {
        throw new UnsupportedOperationException();
    }
 
    @Override
    public String toXMLString(Object object) {
        return object.toString();
    }
 
    @Override
    public Object fromXMLString(String string) {
        return LocalTime.parse(string);
    }
 
}

To use these, you can put the following configuration in your package-info.java file.

@TypeDefs({
        @TypeDef(name = "localDateType",
                defaultForType = LocalDate.class,
                typeClass = LocalDateUserType.class),
        @TypeDef(name = "localDateTimeType",
                defaultForType = LocalDateTime.class,
                typeClass = LocalDateTimeUserType.class),
        @TypeDef(name = "localTimeType",
                defaultForType = LocalTime.class,
                typeClass = LocalTimeUserType.class)
})
package your.domain.pkg;

Make sure you include the package in both the packagesToScan and annotatedPackages configuration in Hibernate.

5 Comments

  1. Jules says:

    What I think is somewhat disturbing is that even though Java 8 is now released and large numbers of new projects are presumably starting to use these types in preference to the old (and somewhat broken) java.util.Date/java.sql.Date/etc, and that the enhancement is pretty simple, it looks as though nobody involved in the Hibernate project has even started work on adding support for them. See https://hibernate.atlassian.net/browse/HHH-8844

  2. Martin says:

    You, Sir, are a star. That problem was doing my box in, so it was.

  3. João Gnome says:

    Awesome! 😀 Thanks for sharing!

  4. Bruno Reis says:

    Congratulations! Resolved!

  5. […] The next option is to use a Hibernate user type (a blog post about converting other JSR-310 classes here). […]

Leave a Reply to João Gnome Cancel reply

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

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*