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

81

Mam tabelę z kolumną typu JSON w mojej bazie danych PostgreSQL (9.2). Trudno jest zmapować tę kolumnę na typ pola Entity JPA2.

Próbowałem użyć String, ale kiedy zapisuję jednostkę, dostaję wyjątek, że nie może konwertować znaków różniących się na JSON.

Jaki jest prawidłowy typ wartości do użycia w przypadku 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.

Ümit
źródło
2
Wiem, że to trochę stare, ale spójrz na moją odpowiedź stackoverflow.com/a/26126168/1535995 na podobne pytanie
Sasa7812
vladmihalcea.com/ ... ten samouczek jest dość prosty
SGuru

Odpowiedzi:

37

Zobacz błąd PgJDBC nr 265 .

PostgreSQL jest nadmiernie i irytująco rygorystyczny w kwestii konwersji typów danych. Nie jest domyślnie rzutowany textnawet na wartości podobne do tekstu, takie jak xmli json.

Dokładnie poprawnym sposobem rozwiązania tego problemu jest napisanie niestandardowego typu odwzorowania Hibernate, który korzysta z setObjectmetody JDBC . Może to być dość kłopotliwe, więc możesz chcieć zmniejszyć restrykcyjność PostgreSQL, tworząc słabszą rzutowanie.

Jak zauważył @markdsievers w komentarzach i tym poście na blogu , oryginalne rozwiązanie w tej odpowiedzi omija walidację JSON. Więc to nie jest to, czego chcesz. Bezpieczniej jest pisać:

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 informuje PostgreSQL, ż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 wskazanie problemu.

Craig Ringer
źródło
2
Warto przeczytać wynikowy wpis na blogu zawierający tę odpowiedź. W szczególności sekcja komentarzy podkreśla niebezpieczeństwa związane z tym (zezwala na nieprawidłowy json) i alternatywne / lepsze rozwiązanie.
markdsievers
@markdsievers Thankyou. Zaktualizowałem post z poprawionym rozwiązaniem.
Craig Ringer
@CraigRinger Nie ma problemu. Dziękuję za obfity wkład w PG / JPA / JDBC, wiele z nich było dla mnie bardzo pomocnych.
markdsievers
1
@CraigRinger Skoro i tak przechodzisz przez cstringkonwersję, nie możesz po prostu użyć CREATE CAST (text AS json) WITH INOUT?
Nick Barnes
@NickBarnes to rozwiązanie również działało idealnie dla mnie (iz tego, co widziałem, zawodzi na nieprawidłowym JSON, tak jak powinno). Dzięki!
zeroDivisible
76

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ępnie zaimplementuj org.hibernate.usertype.UserType. Poniższa implementacja mapuje wartości String na typ bazy danych JSON i odwrotnie. Pamiętaj, że ciągi znaków są niezmienne w Javie. Bardziej złożonej implementacji można by użyć do odwzorowania niestandardowych ziaren Java na JSON przechowywany 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 wszystko, co pozostało, to opisywanie jednostek. Umieść coś takiego w deklaracji klasy jednostki:

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

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

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

Hibernate zajmie się utworzeniem kolumny z typem json i obsłuży mapowanie w obie strony. Wstrzyknij dodatkowe biblioteki do implementacji typu użytkownika, aby uzyskać bardziej zaawansowane mapowanie.

Oto krótki przykładowy projekt GitHub, jeśli ktoś chce się nim bawić:

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

Tim Fulmer
źródło
2
Nie martw się, skończyłem z kodem i tą stroną przede mną i pomyślałem, dlaczego nie :) To może być wada procesu Java. Otrzymujemy całkiem dobrze przemyślane rozwiązania trudnych problemów, ale nie jest łatwo wprowadzić dobry pomysł, taki jak ogólny SPI dla nowych typów. Pozostaje nam wszystko, co wdrażają, w tym przypadku Hibernacja.
Tim Fulmer
3
występuje problem w kodzie implementacji nullSafeGet. Zamiast if (rs.wasNull ()) powinieneś zrobić if (rs.getString (names [0]) == null). Nie jestem pewien, co robi rs.wasNull (), ale w moim przypadku spaliło mnie, zwracając true, gdy wartość, której szukałem, w rzeczywistości nie była zerowa.
rtcarlson,
1
@rtcarlson Niezły chwyt! Przepraszam, że musiałeś przez to przejść. Zaktualizowałem powyższy kod.
Tim Fulmer,
3
To rozwiązanie działało dobrze z Hibernate 4.2.7, z wyjątkiem pobierania wartości null z kolumn json z błędem „Brak mapowania dialektu dla typu JDBC: 1111”. Jednak dodanie następującego wiersza do klasy dialektu rozwiązało problem: this.registerHibernateType (Types.OTHER, "StringJsonUserType");
oliverguenther
7
Nie widzę żadnego kodu w połączonym projekcie github ;-) BTW: Czy nie byłoby przydatne mieć ten kod jako bibliotekę do ponownego wykorzystania?
rü-
21

Jest to bardzo częste pytanie, więc zdecydowałem się napisać bardzo szczegółowy artykuł o najlepszym sposobie mapowania typów kolumn JSON podczas korzystania z JPA i Hibernate.

Zależność Mavena

Pierwszą rzeczą, którą musisz zrobić, jest skonfigurowanie następującej zależności Hibernate Types Maven w pom.xmlpliku konfiguracyjnym projektu :

<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ć JsonBinaryTypena poziomie klasy lub w deskryptorze poziomu pakietu package-info.java , na przykład:

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

Mapowanie jednostek będzie wyglądać następująco:

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

Jeśli używasz Hibernacji 5 lub nowszej, JSONtyp jest rejestrowany automatycznie przezPostgre92Dialect .

W przeciwnym razie musisz to zarejestrować samodzielnie:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

W przypadku MySQL możesz zapoznać się z tym artykułem, aby zobaczyć, jak mapować obiekty JSON przy użyciu JsonStringType.

Vlad Mihalcea
źródło
Fajny przykład, ale czy można go użyć z niektórymi ogólnymi DAO, takimi jak repozytoria Spring Data JPA, do przeszukiwania danych bez natywnych zapytań, tak jak możemy to zrobić z MongoDB? Nie znalazłem żadnej prawidłowej odpowiedzi ani rozwiązania w tej sprawie. Tak, możemy przechowywać dane i możemy je odzyskać, filtrując kolumny w RDBMS, ale do tej pory nie mogę filtrować według kolumn JSONB. Żałuję, że się mylę i jest takie rozwiązanie.
kensai,
Tak, możesz. Ale musisz używać zapytań natywnych, które są również obsługiwane przez Spring Data JPA.
Vlad Mihalcea,
Widzę, to było właściwie moje zadanie, jeśli możemy obejść się bez natywnych zapytań, ale tylko za pomocą metod obiektów. Coś jak adnotacja @Document dla stylu MongoDB. Zakładam więc, że w przypadku PostgreSQL tak nie jest i jedynym rozwiązaniem są natywne zapytania -> paskudne :-), ale dzięki za potwierdzenie.
kensai
Byłoby dobrze widzieć w przyszłości coś w rodzaju encji, która naprawdę reprezentuje adnotacje tabeli i dokumentu w polach typu json i mogę używać repozytoriów Springa do robienia rzeczy CRUD w locie. Myślę, że generuję dość zaawansowane API REST dla baz danych za pomocą Springa. Ale mając JSON na miejscu, mam do czynienia z dość nieoczekiwanym obciążeniem, więc będę musiał przetworzyć każdy dokument z generowaniem zapytań.
kensai
Możesz używać Hibernuj OGM z MongoDB, jeśli JSON jest Twoim pojedynczym sklepem.
Vlad Mihalcea,
12

Jeśli ktoś jest zainteresowany, możesz użyć funkcji JPA 2.1 @Convert/ @Converterz Hibernate. Musiałbyś 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;
wasilij
źródło
Brzmi to przydatne - jakie typy należy konwertować, aby móc pisać w formacie JSON? Czy jest to <MyCustomClass, String> czy inny typ?
myrosia 22.07.15
Dzięki - właśnie zweryfikowałem, że to działa u mnie (JPA 2.1, Hibernate 4.3.10, pgjdbc-ng 0.5, Postgres 9.3)
myrosia
Czy można sprawić, by działało bez podania @Column (columnDefinition = "json") w polu? Hibernate tworzy varchar (255) bez tej definicji.
tfranckiewicz
Hibernate nie może wiedzieć, jakiego typu kolumny chcesz użyć, ale nalegasz, że to Hibernate jest odpowiedzialny za aktualizację schematu bazy danych. Więc chyba wybiera domyślny.
Wasilij
3

Miałem podobny problem z Postgresem (javax.persistence.PersistenceException: org.hibernate.MappingException: No Dialect mapping for JDBC type: 1111) podczas wykonywania zapytań natywnych (przez EntityManager), które pobierały pola json w projekcji, mimo że klasa Entity została z adnotacją TypeDefs. To samo zapytanie przetłumaczone w HQL zostało wykonane bez problemu. Aby rozwiązać ten problem, 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 (z góry, odpowiedź Tim Fulmer).

Balaban Mario
źródło
3

Wypróbowałem wiele metod, które znalazłem w Internecie, większość z nich nie działa, niektóre są zbyt złożone. Poniższy działa dla mnie i jest znacznie prostszy, jeśli nie masz tak ścisłych wymagań dotyczących walidacji typu PostgreSQL.

Ustaw typ łańcucha jdbc PostgreSQL na nieokreślony, na przykład <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

TommyQu
źródło
Dziękuję Ci! Używałem typów hibernacji, ale to jest o wiele łatwiejsze! Do Twojej wiadomości tutaj są dokumenty dotyczące tego parametru jdbc.postgresql.org/documentation/83/connect.html
James,
2

Jest to łatwiejsze do zrobienia, które nie wymaga tworzenia funkcji przy użyciu 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
źródło
Dzięki temu użyłem tego do rzucenia varchar na ltree, działa idealnie.
Vladimir M.
1

Miałem do czynienia z tym i nie chciałem włączać rzeczy za pośrednictwem parametrów połączenia 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. Okazuje się, że wystarczyło określić columnDefinition = "json" w mojej adnotacji @Column.

@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;
nenchev
źródło
3
Gdzie zdefiniowałeś tę klasę HashMapConverter. Jak to wygląda.
sandeep