hibernatejpagrailsgrails-3.0hibernate-5.x

Grails (Hibernate) Mapping of java.time.ZoneId to Database


Is there any way how to support persistent mapping of java.time.ZoneId to string in Hibernate 5.1.1. It saves the ZoneId in binary form right now.

I've just upgraded to Grails 3.2.1 which has Hibernate 5.1.1. Saving of java.time.Instant for example works fine however java.time.ZoneId is stored only in binary form.

I think there is no support from Hibernate. So how can I code my own mapping. I've tried to use Jadira Framework but it is not possible as there are some conflicts (exceptions) when starting the grails app.


Solution

  • So I finally found a nice way how to implement custom hibernate user types. To persist java.time.ZoneId as varchar implement following user type class:

    import org.hibernate.HibernateException
    import org.hibernate.engine.spi.SessionImplementor
    import org.hibernate.type.StandardBasicTypes
    import org.hibernate.usertype.EnhancedUserType
    
    import java.sql.PreparedStatement
    import java.sql.ResultSet
    import java.sql.SQLException
    import java.sql.Types
    import java.time.ZoneId
    
    /**
     * A type that maps between {@link java.sql.Types#VARCHAR} and {@link ZoneId}.
     */
    class ZoneIdUserType implements EnhancedUserType, Serializable {
    
        private static final int[] SQL_TYPES = [Types.VARCHAR]
    
        @Override
        public int[] sqlTypes() {
            return SQL_TYPES
        }
    
        @Override
        public Class returnedClass() {
            return ZoneId.class
        }
    
        @Override
        public boolean equals(Object x, Object y) throws HibernateException {
            if (x == y) {
                return true
            }
            if (x == null || y == null) {
                return false
            }
            ZoneId zx = (ZoneId) x
            ZoneId zy = (ZoneId) y
            return zx.equals(zy)
        }
    
        @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 zoneId = StandardBasicTypes.STRING.nullSafeGet(resultSet, names, session, owner)
            if (zoneId == null) {
                return null
            }
            return ZoneId.of(zoneId)
        }
    
        @Override
        public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
            if (value == null) {
                StandardBasicTypes.STRING.nullSafeSet(preparedStatement, null, index, session)
            } else {
                def zoneId = (ZoneId) value
                StandardBasicTypes.STRING.nullSafeSet(preparedStatement, zoneId.getId(), 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 ZoneId.of(string)
        }
    }
    

    Then you need to register custom user type in conf/application.groovy of your Grails app:

    grails.gorm.default.mapping = {
        'user-type'(type: ZoneIdUserType, class: ZoneId)
    }
    

    Than you can simply use java.time.ZoneId in your domain class:

    import java.time.ZoneId
    
    class MyDomain {
        ZoneId zoneId
    }
    

    See:

    1. http://docs.grails.org/latest/ref/Database%20Mapping/Usage.html
    2. http://blog.progs.be/550/java-time-hibernate