93 votes

Mappage d'une colonne JSON PostgreSQL vers une propriété d'entité Hibernate

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

J'ai essayé d'utiliser String mais lorsque j'enregistre l'entité, je reçois une exception indiquant qu'il ne peut pas convertir les caractères variables en JSON.

Quel est le type de valeur correct à utiliser lorsqu'on traite 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.

87voto

Tim Fulmer Points 411

Si vous êtes intéressé, voici quelques extraits de code pour mettre en place le type d'utilisateur personnalisé Hibernate. Tout d'abord, étendez le dialecte PostgreSQL pour lui indiquer le type json, merci à 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");
    }
}

Implémentez ensuite org.hibernate.usertype.UserType. L'implémentation ci-dessous fait correspondre les valeurs String au type de base de données json, et vice-versa. N'oubliez pas que les chaînes de caractères sont immuables en Java. Une implémentation plus complexe pourrait être utilisée pour faire correspondre des beans Java personnalisés à 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 qu'à annoter les entités. Mettez quelque chose comme ceci dans la déclaration de classe de l'entité :

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

Puis annoter 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 du type d'utilisateur pour un mappage plus avancé.

Voici un exemple rapide de projet GitHub si quelqu'un veut s'amuser avec :

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

39voto

Craig Ringer Points 72371

Voir le bogue PgJDBC #265 .

PostgreSQL est excessivement, ennuyeusement strict sur les conversions de type de données. Il ne mettra pas implicitement text même à des valeurs de type texte telles que xml y json .

La façon strictement correcte de résoudre ce problème est d'écrire un type de mappage Hibernate personnalisé qui utilise la fonction JDBC setObject méthode. Cela peut être un peu compliqué, donc vous pouvez simplement rendre PostgreSQL moins strict en créant un cast plus faible.

Comme l'a fait remarquer @markdsievers dans les commentaires et cet article de blog La solution originale de cette réponse contourne la validation JSON. Ce n'est donc pas vraiment ce que vous voulez. Il 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 qu'on le lui demande explicitement, ce qui permet à 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 d'avoir signalé le problème.

31voto

Vlad Mihalcea Points 3628

Dépendance Maven

La première chose à faire est de mettre en place les éléments suivants Types Hibernate Dépendance Maven dans votre projet pom.xml le fichier de configuration :

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

Modèle de domaine

Maintenant, vous devez déclarer le JsonType au niveau de la classe ou dans une package-info.java descripteur de niveau paquet, comme ceci :

@TypeDef(name = "json", typeClass = JsonType.class)

Et, le mappage de l'entité ressemblera à ceci :

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

Si vous utilisez Hibernate 5 ou une version plus récente, alors l'option JSON est enregistré automatiquement par l Postgre92Dialect .

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

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

16voto

vasily Points 430

Au cas où quelqu'un serait intéressé, vous pouvez utiliser JPA 2.1. @Convert / @Converter avec Hibernate. Vous devrez utiliser la fonction pgjdbc-ng pilote JDBC cependant. Ainsi, vous n'avez pas à utiliser d'extensions propriétaires, de dialectes et de types personnalisés 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;

8voto

TommyQu Points 295

J'ai essayé de nombreuses méthodes que j'ai trouvées sur Internet, la plupart ne fonctionnent pas, certaines sont trop complexes. La méthode ci-dessous fonctionne pour moi et est beaucoup plus simple si vous n'avez pas d'exigences strictes pour la validation de type PostgreSQL.

Rendre le type de chaîne de PostgreSQL jdbc non spécifié, comme suit <connection-url> jdbc:postgresql://localhost:test?stringtype=unspecified </connection-url>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X