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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With