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

3

6

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.

Smokechaser answered 24/10, 2016 at 8:47 Comment(3)
why not saving the zoneId.getId() as string and then init it using ZoneId.of("zoneId") ?Convulsion
This is actually my workaround but somehow I feel that it can be done automatically. At least Jadira was doing it exactly like that (I've used that before upgrading from Grails 3.1.9 to Grails 3.2.1)Smokechaser
I understand, well you can always make a @Transient method in the entity that will do the conversion from string to zone id, so it would be transparentConvulsion
S
1

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
Smokechaser answered 2/11, 2016 at 13:40 Comment(0)
H
14

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.

Hemophilia answered 2/11, 2016 at 10:44 Comment(2)
I've seen this before, however it doesn't work in Grails 3 which uses Hibernate 5. I've found solution implementing my custom User Type. See my answer below. You pointed me to good direction anyway...Smokechaser
Yes, you always can go for a user type, though a converter is much simpler. Do you have any details why it didn't work within Grails 3? I'm surprised about that.Hemophilia
S
2

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)
Sanjuana answered 18/6, 2020 at 11:5 Comment(5)
In my case, I have the Hibernate Types library, and I'm able to use the ZoneId without the @TypeDef! Is this expected?Girish
@Girish Maybe there were some changes in Hibernate types library since this answer was published? Try using this version and tell if it still works.Sanjuana
found the issue, I was not covering this in my integration test, and I thought it was working even when I tried to remove it, but no, it doesn't! thanks anyway :)Girish
@Girish Just to clarify: what's the version of the Hibernate Types library you're using? Does it work without @TypeDef?Sanjuana
Hibernate Types version: 2.16.2. No it doesn't work without @TypeDef :)Girish
S
1

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
Smokechaser answered 2/11, 2016 at 13:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.