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.

3
  • 1
    why not saving the zoneId.getId() as string and then init it using ZoneId.of("zoneId") ? Commented Oct 24, 2016 at 9:07
  • 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) Commented Oct 24, 2016 at 9:09
  • 1
    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 transparent Commented Oct 24, 2016 at 9:18

3 Answers 3

14
+100

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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...
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.
3

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)

5 Comments

In my case, I have the Hibernate Types library, and I'm able to use the ZoneId without the @TypeDef! Is this expected?
@dk7 Maybe there were some changes in Hibernate types library since this answer was published? Try using this version and tell if it still works.
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 :)
@dk7 Just to clarify: what's the version of the Hibernate Types library you're using? Does it work without @TypeDef?
Hibernate Types version: 2.16.2. No it doesn't work without @TypeDef :)
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.