Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Storing a List<SomeClass> as JSON in a text field with Hibernate

Tags:

hibernate

I have something like the following model in Hibernate:

class Person {
    String name;
    List<Address> addresses;
}

class Address {
    String street;
    String city;
}

I now want to persist Person to a table where all the person's addresses are serialized to a JSON string and stored in a column in the Person table. A Person record in the database would look like this:

name: 'Benjamin Franklin', addresses: '[{"street"="...","city"="..."}, {...}]'

Is there a way to achieve this using Hibernate?

If addresses were not a list, I could register a UserType to perform the serialization.

I also cannot use JPA's @Converter, because the Hibernate implementation will not detect changes, see HHH-10111.

like image 490
Jochen Avatar asked Sep 24 '15 15:09

Jochen


1 Answers

You can create a custom type:

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.DynamicParameterizedType;
import org.hibernate.usertype.UserType;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.*;

public class JsonListType implements UserType, DynamicParameterizedType {

    private static final int[] SQL_TYPES = new int[]{Types.LONGVARCHAR};
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private JavaType valueType = null;
    private Class<?> classType = null;

    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }

    @Override
    public Class<?> returnedClass() {
        return classType;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return Objects.equals(x, y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return Objects.hashCode(x);
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
        return nullSafeGet(rs, names, owner);
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
        nullSafeSet(st, value, index);
    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
        String value = rs.getString(names[0]);
        Object result = null;
        if (valueType == null) {
            throw new HibernateException("Value type not set.");
        }
        if (value != null && !value.equals("")) {
            try {
                result = OBJECT_MAPPER.readValue(value, valueType);
            } catch (IOException e) {
                throw new HibernateException("Exception deserializing value " + value, e);
            }
        }
        return result;
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
        StringWriter sw = new StringWriter();
        if (value == null) {
            st.setNull(index, Types.VARCHAR);
        } else {
            try {
                OBJECT_MAPPER.writeValue(sw, value);
                st.setString(index, sw.toString());
            } catch (IOException e) {
                throw new HibernateException("Exception serializing value " + value, e);
            }
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (value == null) {
            return null;
        } else if (valueType.isCollectionLikeType()) {
            try {
                Object newValue = value.getClass().newInstance();
                Collection newValueCollection = (Collection) newValue;
                newValueCollection.addAll((Collection) value);
                return newValueCollection;
            } catch (InstantiationException e) {
                throw new HibernateException("Failed to deep copy the collection-like value object.", e);
            } catch (IllegalAccessException e) {
                throw new HibernateException("Failed to deep copy the collection-like value object.", e);
            }
        }

        return null;
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) deepCopy(value);
    }

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return deepCopy(cached);
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return deepCopy(original);
    }

    @Override
    public void setParameterValues(Properties parameters) {
        try {

            // Get entity class
            Class<?> entityClass = Class.forName(parameters.getProperty(DynamicParameterizedType.ENTITY));
            Field property = null;

            // Find the field
            while(property == null && entityClass != null){
                try {
                    property = entityClass.getDeclaredField(parameters.getProperty(DynamicParameterizedType.PROPERTY));
                } catch (NoSuchFieldException e) {
                    entityClass = entityClass.getSuperclass();
                }
            }

            if(property != null){
                ParameterizedType listType = (ParameterizedType) property.getGenericType();
                Class<?> listClass = (Class<?>) listType.getActualTypeArguments()[0];
                valueType = OBJECT_MAPPER.getTypeFactory().constructCollectionType(ArrayList.class, listClass);
                classType = List.class;
            }

        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }

}

And use it like this:

@Type(type = "com.company.util.JsonListType")
private List<MyCustomClass> myCustomClasses;

This solution is not DB specific and you can easily extend it to support Maps and custom cloneable entities.

like image 132
NikolaB Avatar answered Sep 22 '22 11:09

NikolaB