Storing a <SomeClass> List as JSON in a Text Box with Hibernate
I have something like the following model in Hibernate:
class Person { String name; List<Address> addresses; } class Address { String street; String city; } Now I want to transfer Person to a table where all the character addresses are serialized into a JSON string and stored in the column of the Person table. The Person entry in the database will look like this:
name: 'Benjamin Franklin', addresses: '[{"street"="...","city"="..."}, {...}]' Is there any way to achieve this with Hibernate?
If the addresses were not a list, I could register UserType to perform serialization.
I also cannot use JPA @Converter because the Hibernate implementation will not detect any changes, see HHH-10111 .
You can create your own 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 database independent, and you can easily expand it to support maps and custom cloned objects.
I have not tried myself, but here is a blog worth mentioning ... https://dzone.com/articles/annotating-custom-types
Which basically suggests you add custom annotation. For example, an annotated class calls it "AddressJSONParser" to convert an Address object to JSON (using a parser) and returns it as a String. You should also think about the parser that does the opposite, from the JSON string back to the Address object ...