Mapowanie kolumny PostgreSQL JSON do właściwości encji Hibernate

Mam tabelę z kolumną typu JSON w moim PostgreSQL DB (9.2). Mam problem z mapowaniem tej kolumny na typ pola encji Jpa2.

Próbowałem użyć String, ale kiedy zapisuję encję, dostaję wyjątek, że nie może ona przekonwertować znaków zmieniających się na JSON.

Jaki jest prawidłowy typ wartości do użycia podczas obsługi kolumny JSON?

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

Prostym obejściem byłoby zdefiniowanie kolumny tekstowej.

Author: Vlad Mihalcea, 2013-04-12

8 answers

Zobacz błąd PgJDBC # 265.

PostgreSQL jest nadmiernie irytująco surowy w kwestii konwersji typów danych. Nie będzie ona bezpośrednio rzutować text nawet do wartości tekstowych, takich jak xml i json.

Ściśle poprawnym sposobem rozwiązania tego problemu jest napisanie niestandardowego typu mapowania Hibernate, który używa metody JDBC setObject. Może to być trochę kłopotliwe, więc możesz po prostu chcieć, aby PostgreSQL był mniej rygorystyczny, tworząc słabszą obsadę.

Jak zauważył @markdsievers w komentarze i ten wpis na blogu , oryginalne rozwiązanie w tej odpowiedzi omija walidację JSON. Więc to nie jest to, czego chcesz. Bezpieczniej napisać:

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 mówi Postgresqlowi, że może konwertować bez wyraźnego polecenia, pozwalając na działanie takich rzeczy:

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

Podziękowania dla @ markdsievers za zwrócenie uwagi na problem.

 37
Author: Craig Ringer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-11 07:21:50

Jeśli jesteś zainteresowany, oto kilka fragmentów kodu, aby wprowadzić niestandardowy typ użytkownika Hibernate. Najpierw rozszerz dialekt PostgreSQL, aby powiedzieć mu o typie json, dzięki Craigowi Ringerowi za wskaźnik 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");
    }
}

Następna implementacja org.hibernacja.usertype.UserType. Poniższa implementacja mapuje wartości ciągów do typu bazy danych json i odwrotnie. Pamiętaj, że ciągi są niezmienne w Javie. Bardziej złożoną implementację można wykorzystać do mapowania niestandardowych fasoli Javy do JSON przechowywane również w bazie danych.

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

Teraz pozostaje tylko adnotacja Bytów. Umieść coś takiego w deklaracji klasy encji:

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

Następnie zaznacz właściwość:

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

Hibernate zajmie się tworzeniem kolumny z typem json i zajmie się mapowaniem tam i z powrotem. Wprowadź dodatkowe biblioteki do implementacji typu użytkownika w celu bardziej zaawansowanego mapowania.

Oto krótki przykładowy projekt GitHub, jeśli ktoś chce zagrać around with it:

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

 76
Author: Tim Fulmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-08 18:49:11

Zależność Mavena

Pierwszą rzeczą, którą musisz zrobić, to skonfigurować następujące typy hibernacji zależność Mavena w pliku konfiguracyjnym projektupom.xml:

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

Model domeny

Teraz, jeśli używasz PostgreSQL, musisz zadeklarować JsonBinaryType na poziomie klasy lub w package-info.java deskryptor poziomu pakietu, jak poniżej:

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

I, odwzorowanie encji będzie wyglądało tak:

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

Jeśli używasz Hibernate 5 lub później, wtedy typ JSON jest rejestrowany automatycznie przez Postgre92Dialect.

W przeciwnym razie musisz sam to zarejestrować:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

Dla MySQL, można mapować obiekty JSON za pomocą JsonStringType.

 21
Author: Vlad Mihalcea,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-09 09:43:18

Jeśli ktoś jest zainteresowany, możesz użyć JPA 2.1 @Convert / @Converter funkcjonalność z Hibernate. Musisz jednak użyć sterownika pgjdbc-NG JDBC. W ten sposób nie musisz używać żadnych zastrzeżonych rozszerzeń, dialektów i niestandardowych typów dla każdego pola.

@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;
 12
Author: vasily,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-23 00:21:16

Miałem podobny problem z Postgresem (javax.wytrwałość.PersistenceException: org.hibernacja.MappingException: brak mapowania dialektu dla typu JDBC: 1111) podczas wykonywania natywnych zapytań (poprzez EntityManager), które pobierały pola json w projekcji, chociaż Klasa encji została opatrzona adnotacją TypeDefs. To samo zapytanie przetłumaczone w HQL zostało wykonane bez żadnego problemu. Aby to rozwiązać musiałem zmodyfikować JsonPostgreSQLDialect w ten sposób:

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {

public JsonPostgreSQLDialect() {

    super();

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

Gdzie myCustomType.StringJsonUserType jest nazwą klasy implementującej Typ json (od góry odpowiada Tim Fulmer).

 3
Author: Balaban Mario,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-30 13:09:11

Próbowałem wielu metod, które znalazłem w Internecie, większość z nich nie działa, niektóre są zbyt skomplikowane. Ten poniżej działa dla mnie i jest znacznie prostszy, jeśli nie masz ścisłych wymagań dotyczących walidacji typu PostgreSQL.

Niech PostgreSQL JDBC string type będzie nieokreślony, jak <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

 3
Author: TommyQu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-07 20:22:38

Jest łatwiej to zrobić, co nie wymaga tworzenia funkcji za pomocą 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
 2
Author: Evan Carroll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-03 20:05:32

Wpadłem na to i nie chciałem włączać rzeczy za pomocą łańcucha połączeń i zezwalać na niejawne konwersje. Na początku próbowałem użyć @Type, ale ponieważ używam niestandardowego konwertera do serializacji / deserializacji mapy do / Z JSON, nie mogłem zastosować adnotacji @Type. Okazało się, że po prostu musiałem podać columnDefinition = "json" w mojej adnotacji @Column.

@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;
 1
Author: nenchev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-04-23 22:29:26