Asignación de la columna postgreSQL JSON al tipo de valor Hibernate

Tengo una tabla con una columna de tipo json en mi DB postgreSQL (9.2). Tengo dificultades para asignar esta columna a un tipo de campo de Entidad JPA2.

Intenté usar String pero cuando guardo la entidad recibo una excepción que no puede convertir el carácter que varía a json.

¿Cuál es el tipo de valor correcto para usar cuando se trata de una columna JSON?

@Entity public class MyEntity { private String jsonPayload; // this maps to a json column public MyEntity() { } } 

Una solución simple sería definir una columna de texto.

Ver error PgJDBC # 265 .

PostgreSQL es excesivamente molesto sobre las conversiones de tipo de datos. No lanzará text implícitamente ni siquiera a valores similares a texto como xml y json .

La forma estrictamente correcta de resolver este problema es escribir un tipo de asignación Hibernate personalizado que use el método JDBC setObject . Esto puede ser un poco molesto, por lo que es posible que solo desee que PostgreSQL sea menos estricto al crear un reparto más débil.

Como señaló @markdsievers en los comentarios y esta publicación en el blog , la solución original en esta respuesta omite la validación JSON. Entonces no es realmente lo que quieres. Es más seguro escribir:

 CREATE OR REPLACE FUNCTION json_intext(text) RETURNS json AS $$ SELECT json_in($1::cstring); $$ LANGUAGE SQL IMMUTABLE; CREATE CAST (text AS json) WITH FUNCTION json_intext(text) AS IMPLICIT; 

AS IMPLICIT le dice a PostgreSQL que puede realizar conversiones sin que se lo indique explícitamente, permitiendo que cosas como esta funcionen:

 regress=# CREATE TABLE jsontext(x json); CREATE TABLE regress=# PREPARE test(text) AS INSERT INTO jsontext(x) VALUES ($1); PREPARE regress=# EXECUTE test('{}') INSERT 0 1 

Gracias a @markdsievers por señalar el problema.

Si está interesado, aquí hay algunos fragmentos de código para colocar el tipo de usuario personalizado de Hibernate en su lugar. Primero extienda el dialecto de PostgreSQL para contarle sobre el tipo json, gracias a Craig Ringer para el puntero JAVA_OBJECT:

 import org.hibernate.dialect.PostgreSQL9Dialect; import java.sql.Types; /** * Wrap default PostgreSQL9Dialect with 'json' type. * * @author timfulmer */ public class JsonPostgreSQLDialect extends PostgreSQL9Dialect { public JsonPostgreSQLDialect() { super(); this.registerColumnType(Types.JAVA_OBJECT, "json"); } } 

Siguiente implementa org.hibernate.usertype.UserType. La implementación a continuación asigna los valores de cadena al tipo de base de datos json, y viceversa. Recuerde Las cadenas son inmutables en Java. También se podría usar una implementación más compleja para asignar beans Java personalizados a JSON almacenados en la base de datos.

 package foo; import org.hibernate.HibernateException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.usertype.UserType; import java.io.Serializable; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; /** * @author timfulmer */ public class StringJsonUserType implements UserType { /** * Return the SQL type codes for the columns mapped by this type. The * codes are defined on java.sql.Types. * * @return int[] the typecodes * @see java.sql.Types */ @Override public int[] sqlTypes() { return new int[] { Types.JAVA_OBJECT}; } /** * The class returned by nullSafeGet(). * * @return Class */ @Override public Class returnedClass() { return String.class; } /** * Compare two instances of the class mapped by this type for persistence "equality". * Equality of the persistent state. * * @param x * @param y * @return boolean */ @Override public boolean equals(Object x, Object y) throws HibernateException { if( x== null){ return y== null; } return x.equals( y); } /** * Get a hashcode for the instance, consistent with persistence "equality" */ @Override public int hashCode(Object x) throws HibernateException { return x.hashCode(); } /** * Retrieve an instance of the mapped class from a JDBC resultset. Implementors * should handle possibility of null values. * * @param rs a JDBC result set * @param names the column names * @param session * @param owner the containing entity @return Object * @throws org.hibernate.HibernateException * * @throws java.sql.SQLException */ @Override public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException { if(rs.getString(names[0]) == null){ return null; } return rs.getString(names[0]); } /** * Write an instance of the mapped class to a prepared statement. Implementors * should handle possibility of null values. A multi-column type should be written * to parameters starting from index. * * @param st a JDBC prepared statement * @param value the object to write * @param index statement parameter index * @param session * @throws org.hibernate.HibernateException * * @throws java.sql.SQLException */ @Override public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException { if (value == null) { st.setNull(index, Types.OTHER); return; } st.setObject(index, value, Types.OTHER); } /** * Return a deep copy of the persistent state, stopping at entities and at * collections. It is not necessary to copy immutable objects, or null * values, in which case it is safe to simply return the argument. * * @param value the object to be cloned, which may be null * @return Object a copy */ @Override public Object deepCopy(Object value) throws HibernateException { return value; } /** * Are objects of this type mutable? * * @return boolean */ @Override public boolean isMutable() { return true; } /** * Transform the object into its cacheable representation. At the very least this * method should perform a deep copy if the type is mutable. That may not be enough * for some implementations, however; for example, associations must be cached as * identifier values. (optional operation) * * @param value the object to be cached * @return a cachable representation of the object * @throws org.hibernate.HibernateException * */ @Override public Serializable disassemble(Object value) throws HibernateException { return (String)this.deepCopy( value); } /** * Reconstruct an object from the cacheable representation. At the very least this * method should perform a deep copy if the type is mutable. (optional operation) * * @param cached the object to be cached * @param owner the owner of the cached object * @return a reconstructed object from the cachable representation * @throws org.hibernate.HibernateException * */ @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return this.deepCopy( cached); } /** * During merge, replace the existing (target) value in the entity we are merging to * with a new (original) value from the detached entity we are merging. For immutable * objects, or null values, it is safe to simply return the first parameter. For * mutable objects, it is safe to return a copy of the first parameter. For objects * with component values, it might make sense to recursively replace component values. * * @param original the value from the detached entity being merged * @param target the value in the managed entity * @return the value to be merged */ @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } } 

Ahora todo lo que queda es anotar las entidades. Pon algo así en la statement de clase de la entidad:

 @TypeDefs( {@TypeDef( name= "StringJsonObject", typeClass = StringJsonUserType.class)}) 

Luego anota la propiedad:

 @Type(type = "StringJsonObject") public String getBar() { return bar; } 

Hibernate se encargará de crear la columna con el tipo json para usted, y manejará el mapeo hacia adelante y hacia atrás. Inyecte bibliotecas adicionales en la implementación del tipo de usuario para una asignación más avanzada.

Aquí hay una muestra rápida del proyecto GitHub si alguien quiere jugar con ella:

https://github.com/timfulmer/hibernate-postgres-jsontype

En caso de que alguien esté interesado, puede usar la funcionalidad JPA 2.1 @Convert / @Converter con Hibernate. Sin embargo, debería usar el controlador pgjdbc-ng JDBC. De esta forma, no tiene que usar extensiones, dialectos y tipos personalizados de propiedad por campo.

 @javax.persistence.Converter public static class MyCustomConverter implements AttributeConverter { @Override @NotNull public String convertToDatabaseColumn(@NotNull MuCustomClass myCustomObject) { ... } @Override @NotNull public MuCustomClass convertToEntityAttribute(@NotNull String databaseDataAsJSONString) { ... } } ... @Convert(converter = MyCustomConverter.class) private MyCustomClass attribute; 

Como expliqué en este artículo , es muy fácil persistir el objeto JSON usando Hibernate.

No tiene que crear todos estos tipos manualmente, simplemente puede obtenerlos a través de Maven Central usando la siguiente dependencia:

  com.vladmihalcea hibernate-types-52 ${hibernate-types.version}  

Para obtener más información, consulte el proyecto de código abierto hibernate-types .

Ahora, para explicar cómo funciona todo.

Escribí un artículo sobre cómo puede mapear objetos JSON en PostgreSQL y MySQL.

Para PostgreSQL, debe enviar el objeto JSON en forma binaria:

 public class JsonBinaryType extends AbstractSingleColumnStandardBasicType implements DynamicParameterizedType { public JsonBinaryType() { super( JsonBinarySqlTypeDescriptor.INSTANCE, new JsonTypeDescriptor() ); } public String getName() { return "jsonb"; } @Override public void setParameterValues(Properties parameters) { ((JsonTypeDescriptor) getJavaTypeDescriptor()) .setParameterValues(parameters); } } 

El JsonBinarySqlTypeDescriptor ve así:

 public class JsonBinarySqlTypeDescriptor extends AbstractJsonSqlTypeDescriptor { public static final JsonBinarySqlTypeDescriptor INSTANCE = new JsonBinarySqlTypeDescriptor(); @Override public  ValueBinder getBinder( final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder(javaTypeDescriptor, this) { @Override protected void doBind( PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { st.setObject(index, javaTypeDescriptor.unwrap( value, JsonNode.class, options), getSqlType() ); } @Override protected void doBind( CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { st.setObject(name, javaTypeDescriptor.unwrap( value, JsonNode.class, options), getSqlType() ); } }; } } 

y el JsonTypeDescriptor así:

 public class JsonTypeDescriptor extends AbstractTypeDescriptor implements DynamicParameterizedType { private Class jsonObjectClass; @Override public void setParameterValues(Properties parameters) { jsonObjectClass = ( (ParameterType) parameters.get( PARAMETER_TYPE ) ) .getReturnedClass(); } public JsonTypeDescriptor() { super( Object.class, new MutableMutabilityPlan() { @Override protected Object deepCopyNotNull(Object value) { return JacksonUtil.clone(value); } }); } @Override public boolean areEqual(Object one, Object another) { if ( one == another ) { return true; } if ( one == null || another == null ) { return false; } return JacksonUtil.toJsonNode(JacksonUtil.toString(one)).equals( JacksonUtil.toJsonNode(JacksonUtil.toString(another))); } @Override public String toString(Object value) { return JacksonUtil.toString(value); } @Override public Object fromString(String string) { return JacksonUtil.fromString(string, jsonObjectClass); } @SuppressWarnings({ "unchecked" }) @Override public  X unwrap(Object value, Class type, WrapperOptions options) { if ( value == null ) { return null; } if ( String.class.isAssignableFrom( type ) ) { return (X) toString(value); } if ( Object.class.isAssignableFrom( type ) ) { return (X) JacksonUtil.toJsonNode(toString(value)); } throw unknownUnwrap( type ); } @Override public  Object wrap(X value, WrapperOptions options) { if ( value == null ) { return null; } return fromString(value.toString()); } } 

Ahora, debe declarar el nuevo tipo en cualquier nivel de clase o en un paquete -info.java package-level descriptior:

 @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) 

Y el mapeo de la entidad se verá así:

 @Type(type = "jsonb") @Column(columnDefinition = "json") private Location location; 

Si está utilizando Hibernate 5 o posterior, el tipo JSON es registrado automáticamente por Postgre92Dialect .

De lo contrario, debe registrarlo usted mismo:

 public class PostgreSQLDialect extends PostgreSQL91Dialect { public PostgreSQL92Dialect() { super(); this.registerColumnType( Types.JAVA_OBJECT, "json" ); } } 

Tuve un problema similar con Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: No Dialect mapping for JDBC type: 1111) al ejecutar consultas nativas (a través de EntityManager) que recuperaban campos json en la proyección, aunque la clase Entity ha sido anotado con TypeDefs. La misma consulta traducida en HQL se ejecutó sin ningún problema. Para resolver esto tuve que modificar JsonPostgreSQLDialect de esta manera:

 public class JsonPostgreSQLDialect extends PostgreSQL9Dialect { public JsonPostgreSQLDialect() { super(); this.registerColumnType(Types.JAVA_OBJECT, "json"); this.registerHibernateType(Types.OTHER, "myCustomType.StringJsonUserType"); } 

Donde myCustomType.StringJsonUserType es el nombre de clase de la clase que implementa el tipo json (desde arriba, respuesta de Tim Fulmer).

Probé muchos métodos que encontré en Internet, la mayoría de ellos no funcionan, algunos de ellos son demasiado complejos. El siguiente funciona para mí y es mucho más simple si no tienes los estrictos requisitos para la validación de tipo PostgreSQL.

Haga que el tipo de cadena jdbc de PostgreSQL no esté especificado, como jdbc:postgresql://localhost:test?stringtype=‌​unspecified

Es más fácil hacer esto que no implica crear una función usando WITH INOUT

 CREATE TABLE jsontext(x json); INSERT INTO jsontext VALUES ($${"a":1}$$::text); ERROR: column "x" is of type json but expression is of type text LINE 1: INSERT INTO jsontext VALUES ($${"a":1}$$::text); CREATE CAST (text AS json) WITH INOUT AS ASSIGNMENT; INSERT INTO jsontext VALUES ($${"a":1}$$::text); INSERT 0 1