Asignación de la columna JSON de PostgreSQL a una propiedad de entidad de Hibernate

81

Tengo una tabla con una columna de tipo JSON en mi PostgreSQL DB (9.2). Me cuesta asignar esta columna a un tipo de campo de entidad JPA2.

Intenté usar String pero cuando guardo la entidad obtengo una excepción de que no puede convertir caracteres que varían 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 alternativa sencilla sería definir una columna de texto.

Ümit
fuente
2
Sé que esto es un poco antiguo, pero eche un vistazo a mi respuesta stackoverflow.com/a/26126168/1535995 para una pregunta similar
Sasa7812
vladmihalcea.com/… este tutorial es bastante sencillo
SGuru

Respuestas:

37

Consulte el error # 265 de PgJDBC .

PostgreSQL es excesivamente estricto con las conversiones de tipos de datos. No se convertirá implícitamente textni siquiera en valores de texto como xmly json.

La forma estrictamente correcta de resolver este problema es escribir un tipo de mapeo de Hibernate personalizado que use el setObjectmétodo JDBC . Esto puede ser un poco complicado, por lo que es posible que desee hacer que PostgreSQL sea menos estricto creando un reparto más débil.

Como señaló @markdsievers en los comentarios y esta publicación de 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 convertir sin que se le diga 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.

Craig Ringer
fuente
2
Vale la pena leer la publicación de blog resultante de esta respuesta. En particular, la sección de comentarios destaca los peligros de esto (permite json inválido) y la solución alternativa / superior.
markdsievers
@markdsievers Gracias. Actualicé la publicación con una solución corregida.
Craig Ringer
@CraigRinger No hay problema. Gracias por sus prolíficas contribuciones PG / JPA / JDBC, muchas me han sido de gran ayuda.
markdsievers
1
@CraigRinger Ya que está pasando por la cstringconversión de todos modos, ¿no podría simplemente usar CREATE CAST (text AS json) WITH INOUT?
Nick Barnes
@NickBarnes esa solución también funcionó perfectamente para mí (y por lo que había visto, falla en JSON no válido, como debería). ¡Gracias!
zeroDivisible
76

Si está interesado, aquí hay algunos fragmentos de código para implementar el tipo de usuario personalizado de Hibernate. Primero amplíe el dialecto de PostgreSQL para informarle sobre el tipo json, gracias a Craig Ringer por 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");
    }
}

A continuación, implemente org.hibernate.usertype.UserType. La siguiente implementación asigna valores de cadena al tipo de base de datos json y viceversa. Recuerde que las cadenas son inmutables en Java. También se podría utilizar una implementación más compleja para mapear 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 <tt>java.sql.Types</tt>.
     *
     * @return int[] the typecodes
     * @see java.sql.Types
     */
    @Override
    public int[] sqlTypes() {
        return new int[] { Types.JAVA_OBJECT};
    }

    /**
     * The class returned by <tt>nullSafeGet()</tt>.
     *
     * @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 <tt>index</tt>.
     *
     * @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 como esto en la declaración de clase de la entidad:

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

Luego anote la propiedad:

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

Hibernate se encargará de crear la columna con el tipo json por usted y manejará el mapeo de un lado a otro. Inyecte bibliotecas adicionales en la implementación del tipo de usuario para un mapeo más avanzado.

Aquí hay una muestra rápida de un proyecto de GitHub si alguien quiere jugar con él:

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

Tim Fulmer
fuente
2
No se preocupen chicos, terminé con el código y esta página frente a mí y pensé por qué no :) Esa podría ser la desventaja del proceso de Java. Tenemos algunas soluciones bastante bien pensadas para problemas difíciles, pero no es fácil entrar y agregar una buena idea como SPI genérico para nuevos tipos. Nos quedamos con lo que los implementadores, Hibernate en este caso, pusieron en su lugar.
Tim Fulmer
3
hay un problema en su código de implementación para nullSafeGet. En lugar de if (rs.wasNull ()) debe hacer if (rs.getString (names [0]) == null). No estoy seguro de lo que hace rs.wasNull (), pero en mi caso me quemó al devolver verdadero, cuando el valor que estaba buscando no era nulo.
rtcarlson
1
@rtcarlson ¡Buena captura! Lamento que hayas tenido que pasar por eso. Actualicé el código anterior.
Tim Fulmer
3
Esta solución funcionó bien con Hibernate 4.2.7, excepto cuando se recuperaron nulos de las columnas json con el error 'No hay asignación de dialecto para el tipo JDBC: 1111'. Sin embargo, agregar la siguiente línea a la clase de dialecto lo solucionó: this.registerHibernateType (Types.OTHER, "StringJsonUserType");
oliverguenther
7
No veo ningún código en el proyecto github vinculado ;-) Por cierto: ¿No sería útil tener este código como una biblioteca para su reutilización?
rü-
21

Esta es una pregunta muy común, por lo que decidí escribir un artículo muy detallado sobre la mejor manera de mapear los tipos de columnas JSON al usar JPA e Hibernate.

Dependencia de Maven

Lo primero que debe hacer es configurar la siguiente dependencia de Hibernate Type Maven en el pom.xmlarchivo de configuración de su proyecto :

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

Modelo de dominio

Ahora, si está utilizando PostgreSQL, debe declarar el JsonBinaryTypenivel de clase o en un descriptor de nivel de paquete package-info.java , como este:

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

Y el mapeo de entidades se verá así:

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

Si está utilizando Hibernate 5 o posterior, el JSONtipo es registrado automáticamente porPostgre92Dialect .

De lo contrario, debe registrarlo usted mismo:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

Para MySQL, puede consultar este artículo para ver cómo puede mapear objetos JSON usando JsonStringType.

Vlad Mihalcea
fuente
Buen ejemplo, pero ¿se puede usar esto con algún DAO genérico, como los repositorios Spring Data JPA para consultar datos sin consultas nativas como podemos hacer con MongoDB? No encontré ninguna respuesta o solución válida para este caso. Sí, podemos almacenar los datos y podemos recuperarlos filtrando columnas en RDBMS, pero hasta ahora no puedo filtrar por JSONB coluns. Desearía estar equivocado y existe esa solución.
kensai
Sí tu puedes. Pero debe usar consultas nativas que también son compatibles con Spring Data JPA.
Vlad Mihalcea
Ya veo, esa era en realidad mi pregunta, si podemos ir sin consultas nativas, pero solo a través de métodos de objetos. Algo como la anotación @Document para el estilo MongoDB. Así que supongo que esto no es tan lejos en el caso de PostgreSQL y la única solución son las consultas nativas -> desagradable :-), pero gracias por la confirmación.
kensai
Sería bueno ver en el futuro algo como una entidad que realmente representa la anotación de tabla y documento en campos tipo json y puedo usar los repositorios de Spring para hacer cosas CRUD sobre la marcha. Creo que estoy generando una API REST bastante avanzada para bases de datos con Spring. Pero con JSON en su lugar, me enfrento a una sobrecarga bastante inesperada, por lo que también tendré que procesar cada documento con consultas de generación.
kensai
Puede usar Hibernate OGM con MongoDB si JSON es su única tienda.
Vlad Mihalcea
12

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

@javax.persistence.Converter
public static class MyCustomConverter implements AttributeConverter<MuCustomClass, String> {

    @Override
    @NotNull
    public String convertToDatabaseColumn(@NotNull MuCustomClass myCustomObject) {
        ...
    }

    @Override
    @NotNull
    public MuCustomClass convertToEntityAttribute(@NotNull String databaseDataAsJSONString) {
        ...
    }
}

...

@Convert(converter = MyCustomConverter.class)
private MyCustomClass attribute;
vasily
fuente
Esto suena útil: ¿entre qué tipos debería convertir para poder escribir JSON? ¿Es <MyCustomClass, String> o algún otro tipo?
myrosia
Gracias, acabo de verificar que funciona para mí (JPA 2.1, Hibernate 4.3.10, pgjdbc-ng 0.5, Postgres 9.3)
myrosia
¿Es posible hacer que funcione sin especificar @Column (columnDefinition = "json") en el campo? Hibernate está haciendo un varchar (255) sin esta definición.
tfranckiewicz
Hibernate no puede saber qué tipo de columna desea allí, pero insiste en que es responsabilidad de Hibernate actualizar el esquema de la base de datos. Así que supongo que elige el predeterminado.
vasily
3

Tuve un problema similar con Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: No hay asignación de dialecto para el tipo JDBC: 1111) al ejecutar consultas nativas (a través de EntityManager) que recuperaron 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, la respuesta de Tim Fulmer).

Mario Balaban
fuente
3

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 tiene requisitos tan estrictos para la validación de tipo PostgreSQL.

Haga que el tipo de cadena jdbc de PostgreSQL no esté especificado, como <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

TommyQu
fuente
¡Gracias! Estaba usando tipos de hibernación, ¡pero esto es mucho más fácil! Para su información, aquí están los documentos sobre este parámetro jdbc.postgresql.org/documentation/83/connect.html
James
2

Hay una forma más fácil de hacer esto que no implica la creación de 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
Evan Carroll
fuente
Gracias, usé esto para lanzar varchar a ltree, funciona perfectamente.
Vladimir M.
1

Me estaba encontrando con esto y no quería habilitar cosas a través de la cadena de conexión y permitir conversiones implícitas. Al principio intenté usar @Type, pero como estoy usando un convertidor personalizado para serializar / deserializar un mapa hacia / desde JSON, no pude aplicar una anotación @Type. Resulta que solo necesitaba especificar columnDefinition = "json" en mi anotación @Column.

@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;
nenchev
fuente
3
¿Dónde ha definido esta clase HashMapConverter? ¿Cómo se ve?
sandeep