kuceram
kuceram

Reputation: 3885

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.

Upvotes: 6

Views: 3478

Answers (3)

izogfif
izogfif

Reputation: 7495

You can use Hibernate types library and then just write

@Column
private ZoneId zoneId;

in your entity classes. You have to mark the entity class with this annotation:

@TypeDef(typeClass = ZoneIdType.class, defaultForType = ZoneId.class)

Upvotes: 3

kuceram
kuceram

Reputation: 3885

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

Upvotes: 1

Gunnar
Gunnar

Reputation: 18990

You can use a custom attribute converter as defined by JPA 2.1. Declare the converter class like so:

@Converter
public static class ZoneIdConverter implements AttributeConverter<ZoneId, String> {

    @Override
    public String convertToDatabaseColumn(ZoneId attribute) {
        return attribute.getId();
    }

    @Override
    public ZoneId convertToEntityAttribute(String dbData) {
        return ZoneId.of( dbData );
    }
}

And then refer to it from the entity attribute of type ZoneId:

@Convert(converter = ZoneIdConverter.class)
private ZoneId zoneId;

The converter will automatically be invoked when persisting/loading the zoneId attribute.

Upvotes: 14

Related Questions