web-dev-qa-db-fra.com

Mappage de la colonne PostgreSQL JSON au type de valeur Hibernate

J'ai une table avec une colonne de type JSON dans ma base de données PostgreSQL (9.2). J'ai du mal à mapper cette colonne sur un type de champ Entité JPA2.

J'ai essayé d'utiliser String, mais lorsque je sauvegarde l'entité, je reçois une exception selon laquelle il ne peut pas convertir un caractère variant en JSON.

Quel est le type de valeur correct à utiliser pour traiter une colonne JSON?

@Entity
public class MyEntity {

    private String jsonPayload; // this maps to a json column

    public MyEntity() {
    }
}

Une solution de contournement simple consisterait à définir une colonne de texte.

71
Ümit

Voir le bogue PgJDBC n ° 265 .

PostgreSQL est excessivement, extrêmement agaçant quant aux conversions de types de données. Il ne renvoie pas implicitement text même à des valeurs de type texte telles que xml et json.

La façon la plus correcte de résoudre ce problème consiste à écrire un type de mappage personnalisé Hibernate utilisant la méthode JDBC setObject. Cela peut être un peu fastidieux, vous voudrez peut-être simplement rendre PostgreSQL moins strict en créant une distribution plus faible.

Comme noté par @markdsievers dans les commentaires et cet article de blog , la solution d'origine dans cette réponse contourne la validation JSON. Donc ce n'est pas vraiment ce que vous voulez. C'est plus sûr d'écrire:

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 indique à PostgreSQL qu'il peut convertir sans se faire indiquer explicitement, permettant ainsi à des choses comme celle-ci de fonctionner:

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

Merci à @markdsievers pour avoir signalé le problème.

36
Craig Ringer

Si cela vous intéresse, voici quelques extraits de code permettant de définir le type d'utilisateur personnalisé Hibernate. Tout d’abord, étendez le dialecte PostgreSQL pour parler du type json, grâce à Craig Ringer pour le pointeur 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");
    }
}

Ensuite, implémentez org.hibernate.usertype.UserType. L'implémentation ci-dessous mappe les valeurs de chaîne sur le type de base de données json, et inversement. Rappelez-vous que les chaînes sont immuables en Java. Une implémentation plus complexe pourrait également être utilisée pour mapper des beans Java) personnalisés au format JSON stocké dans la base de données.

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;
    }
}

Il ne reste plus maintenant qu’à annoter les entités. Mettez quelque chose comme ceci à la déclaration de classe de l'entité:

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

Puis annotez la propriété:

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

Hibernate se chargera de créer la colonne de type json pour vous et de gérer le mappage dans les deux sens. Injectez des bibliothèques supplémentaires dans l'implémentation de type utilisateur pour un mappage plus avancé.

Voici un exemple de projet GitHub si vous voulez jouer avec:

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

74
Tim Fulmer

Comme je l'ai expliqué dans cet article , il est très facile de conserver un objet JSON à l'aide de Hibernate.

Il n'est pas nécessaire de créer manuellement tous ces types, vous pouvez simplement les obtenir via Maven Central en utilisant la dépendance suivante:

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

Pour plus d'informations, consultez le projet open-source hibernate-types .

Maintenant, pour expliquer comment tout cela fonctionne.

J'ai écrit n article sur la manière de mapper des objets JSON à la fois sur PostgreSQL et MySQL.

Pour PostgreSQL, vous devez envoyer l'objet JSON sous une forme binaire:

public class JsonBinaryType
    extends AbstractSingleColumnStandardBasicType<Object> 
    implements DynamicParameterizedType {

    public JsonBinaryType() {
        super( 
            JsonBinarySqlTypeDescriptor.INSTANCE, 
            new JsonTypeDescriptor()
        );
    }

    public String getName() {
        return "jsonb";
    }

    @Override
    public void setParameterValues(Properties parameters) {
        ((JsonTypeDescriptor) getJavaTypeDescriptor())
            .setParameterValues(parameters);
    }

}

Le JsonBinarySqlTypeDescriptor ressemble à ceci:

public class JsonBinarySqlTypeDescriptor
    extends AbstractJsonSqlTypeDescriptor {

    public static final JsonBinarySqlTypeDescriptor INSTANCE = 
        new JsonBinarySqlTypeDescriptor();

    @Override
    public <X> ValueBinder<X> getBinder(
        final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>(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()
                );
            }
        };
    }
}

et le JsonTypeDescriptor comme ceci:

public class JsonTypeDescriptor
        extends AbstractTypeDescriptor<Object> 
        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<Object>() {
            @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> X unwrap(Object value, Class<X> 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 <X> Object wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        return fromString(value.toString());
    }

}

Maintenant, vous devez déclarer le nouveau type au niveau de la classe ou dans un descriptior package-info.Java au niveau du package:

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

Et le mappage des entités ressemblera à ceci:

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

Si vous utilisez Hibernate version 5 ou ultérieure, le type JSON est enregistré automatiquement par Postgre92Dialect .

Sinon, vous devez l'enregistrer vous-même:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

    public PostgreSQL92Dialect() {
        super();
        this.registerColumnType( Types.Java_OBJECT, "json" );
    }
}
13
Vlad Mihalcea

Si quelqu'un est intéressé, vous pouvez utiliser JPA 2.1 @Convert/@Converter fonctionnalité avec Hibernate. Vous devrez cependant utiliser le pilote JDBC pgjdbc-ng. De cette façon, vous ne devez utiliser aucune extension, dialecte ou type personnalisé par champ.

@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;
11
vasily

J'ai eu un problème similaire avec Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: aucun mappage de dialecte pour le type JDBC: 1111) lors de l'exécution de requêtes natives (via EntityManager) qui récupéraient des champs json dans la projection bien que la classe Entity ait été annoté avec TypeDefs. La même requête traduite en HQL a été exécutée sans problème. Pour résoudre cela, j'ai dû modifier JsonPostgreSQLDialect de cette façon:

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {

public JsonPostgreSQLDialect() {

    super();

    this.registerColumnType(Types.Java_OBJECT, "json");
    this.registerHibernateType(Types.OTHER, "myCustomType.StringJsonUserType");
}

Où myCustomType.StringJsonUserType est le nom de la classe implémentant le type JSON (en haut, réponse Tim Fulmer).

3
Balaban Mario

J'ai essayé de nombreuses méthodes trouvées sur Internet, la plupart d'entre elles ne fonctionnaient pas et certaines étaient trop complexes. Celui ci-dessous fonctionne pour moi et est beaucoup plus simple si vous n’avez pas cette exigence stricte pour la validation de type PostgreSQL.

Définissez le type de chaîne jdbc de PostgreSQL comme non spécifié, comme <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

2
TommyQu

Il est plus facile de faire cela sans créer de fonction en utilisant 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
1
Evan Carroll