Zuordnen der PostgreSQL-JSON-Spalte zu einer Entitätseigenschaft im Ruhezustand

81

Ich habe eine Tabelle mit einer Spalte vom Typ JSON in meiner PostgreSQL-Datenbank (9.2). Es fällt mir schwer, diese Spalte einem JPA2-Entitätsfeldtyp zuzuordnen.

Ich habe versucht, String zu verwenden, aber wenn ich die Entität speichere, erhalte ich eine Ausnahme, dass Zeichen, die variieren, nicht in JSON konvertiert werden können.

Was ist der richtige Wertetyp für eine JSON-Spalte?

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

Eine einfache Problemumgehung wäre das Definieren einer Textspalte.

Ümit
quelle
2
Ich weiß, dass dies ein bisschen alt ist, aber werfen Sie einen Blick auf meine Antwort stackoverflow.com/a/26126168/1535995 für die ähnliche Frage
Sasa7812
vladmihalcea.com/… dieses Tutorial ist ziemlich einfach
SGuru

Antworten:

37

Siehe PgJDBC-Fehler Nr. 265 .

PostgreSQL ist in Bezug auf Datentypkonvertierungen übermäßig streng. Es wird nicht implizit textin textähnliche Werte wie xmlund umgewandelt json.

Die genau richtige Methode zur Lösung dieses Problems besteht darin, einen benutzerdefinierten Zuordnungstyp für den Ruhezustand zu schreiben, der die JDBC- setObjectMethode verwendet. Dies kann ein ziemlicher Aufwand sein, daher möchten Sie PostgreSQL möglicherweise weniger streng gestalten, indem Sie eine schwächere Besetzung erstellen.

Wie von @markdsievers in den Kommentaren und in diesem Blogbeitrag angegeben , umgeht die ursprüngliche Lösung in dieser Antwort die JSON-Validierung. Es ist also nicht wirklich das, was du willst. Es ist sicherer zu schreiben:

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 teilt PostgreSQL mit, dass es konvertieren kann, ohne explizit dazu aufgefordert zu werden, sodass solche Dinge funktionieren:

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

Vielen Dank an @markdsievers für den Hinweis auf das Problem.

Craig Ringer
quelle
2
Es lohnt sich, den resultierenden Blog-Beitrag dieser Antwort zu lesen . Insbesondere der Kommentarbereich hebt die Gefahren dieses (erlaubt ungültigen JSON) und der alternativen / überlegenen Lösung hervor.
Markdsievers
@markdsievers Danke. Ich habe den Beitrag mit einer korrigierten Lösung aktualisiert.
Craig Ringer
@CraigRinger Kein Problem. Vielen Dank für Ihre produktiven Beiträge zu PG / JPA / JDBC. Viele haben mir sehr geholfen.
Markdsievers
1
@CraigRinger Da Sie die cstringKonvertierung sowieso durchlaufen , können Sie sie nicht einfach verwenden CREATE CAST (text AS json) WITH INOUT?
Nick Barnes
@ NickBarnes diese Lösung funktionierte auch perfekt für mich (und nach allem, was ich gesehen hatte, schlägt sie bei ungültigem JSON fehl, wie es sollte). Vielen Dank!
zeroDivisible
76

Wenn Sie interessiert sind, finden Sie hier einige Codeausschnitte, mit denen Sie den benutzerdefinierten Benutzertyp für den Ruhezustand einrichten können. Erweitern Sie zuerst den PostgreSQL-Dialekt, um ihn über den json-Typ zu informieren, dank Craig Ringer für den JAVA_OBJECT-Zeiger:

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

Als nächstes implementieren Sie org.hibernate.usertype.UserType. Die folgende Implementierung ordnet String-Werte dem json-Datenbanktyp zu und umgekehrt. Denken Sie daran, dass Strings in Java unveränderlich sind. Eine komplexere Implementierung könnte verwendet werden, um benutzerdefinierte Java-Beans auch JSON zuzuordnen, das in der Datenbank gespeichert ist.

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

Jetzt müssen nur noch die Entitäten mit Anmerkungen versehen werden. Fügen Sie so etwas in die Klassendeklaration der Entität ein:

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

Kommentieren Sie dann die Eigenschaft:

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

Hibernate kümmert sich darum, die Spalte mit dem Typ json für Sie zu erstellen, und übernimmt die Zuordnung hin und her. Fügen Sie der Benutzertypimplementierung zusätzliche Bibliotheken für eine erweiterte Zuordnung hinzu.

Hier ist ein kurzes Beispiel für ein GitHub-Projekt, wenn jemand damit herumspielen möchte:

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

Tim Fulmer
quelle
2
Keine Sorge Leute, ich habe den Code und diese Seite vor mir gefunden und herausgefunden, warum nicht :) Das könnte der Nachteil des Java-Prozesses sein. Wir bekommen einige ziemlich gut durchdachte Lösungen für schwierige Probleme, aber es ist nicht einfach, eine gute Idee wie generisches SPI für neue Typen hinzuzufügen. Wir haben alles übrig, was die Implementierer, in diesem Fall Hibernate, eingerichtet haben.
Tim Fulmer
3
Es gibt ein Problem in Ihrem Implementierungscode für nullSafeGet. Anstelle von if (rs.wasNull ()) sollten Sie if (rs.getString (names [0]) == null) ausführen. Ich bin nicht sicher, was rs.wasNull () tut, aber in meinem Fall hat es mich verbrannt, indem es true zurückgegeben hat, als der gesuchte Wert tatsächlich nicht null war.
Rtcarlson
1
@rtcarlson Schöner Fang! Tut mir leid, dass du das durchmachen musstest. Ich habe den obigen Code aktualisiert.
Tim Fulmer
3
Diese Lösung funktionierte gut mit Hibernate 4.2.7, außer beim Abrufen von Null aus JSON-Spalten mit dem Fehler 'Keine Dialektzuordnung für JDBC-Typ: 1111'. Das Hinzufügen der folgenden Zeile zur Dialektklasse hat dies jedoch behoben: this.registerHibernateType (Types.OTHER, "StringJsonUserType");
Oliverguenther
7
Ich sehe keinen Code im verknüpften Github-Projekt ;-) Übrigens : Wäre es nicht nützlich, diesen Code als Bibliothek zur Wiederverwendung zu haben?
rü-
19

Dies ist eine sehr häufige Frage, daher habe ich beschlossen, einen sehr detaillierten Artikel darüber zu schreiben , wie JSON-Spaltentypen bei Verwendung von JPA und Hibernate am besten zugeordnet werden können.

Maven-Abhängigkeit

Das erste , was Sie tun müssen, ist die folgende einzurichten Hibernate Typen Maven Abhängigkeit in Ihrer Projekt - pom.xmlKonfigurationsdatei:

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

Domänenmodell

Wenn Sie jetzt PostgreSQL verwenden, müssen Sie das JsonBinaryTypeentweder auf Klassenebene oder in einem Deskriptor auf Paketebene von package-info.java deklarieren , wie folgt :

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

Die Entitätszuordnung sieht folgendermaßen aus:

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

Wenn Sie Hibernate 5 oder höher verwenden, wird der JSONTyp automatisch von der registriertPostgre92Dialect .

Andernfalls müssen Sie es selbst registrieren:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

In MySQL können Sie in diesem Artikel nachlesen, wie Sie JSON-Objekte mithilfe von MySQL zuordnen können JsonStringType.

Vlad Mihalcea
quelle
Nettes Beispiel, aber kann dies mit einigen generischen DAOs wie Spring Data JPA-Repositorys verwendet werden, um Daten ohne native Abfragen abzufragen, wie wir es mit MongoDB tun können? Ich habe keine gültige Antwort oder Lösung für diesen Fall gefunden. Ja, wir können die Daten speichern und sie durch Filtern von Spalten in RDBMS abrufen, aber ich kann bisher nicht nach JSONB-Spalten filtern. Ich wünschte, ich liege falsch und es gibt eine solche Lösung.
Kensai
Ja, du kannst. Sie müssen jedoch native Abfragen verwenden, die auch von Spring Data JPA unterstützt werden.
Vlad Mihalcea
Ich sehe, das war eigentlich meine Aufgabe, wenn wir auf native Abfragen verzichten können, sondern nur über Objektmethoden. So etwas wie eine @ Document-Annotation für den MongoDB-Stil. Ich gehe also davon aus, dass dies bei PostgreSQL nicht so weit ist und die einzige Lösung native Abfragen sind -> böse :-), aber danke für die Bestätigung.
Kensai
Es wäre gut, in Zukunft so etwas wie eine Entität zu sehen, die wirklich Tabellen- und Dokumentanmerkungen für Felder vom Typ json darstellt, und ich kann Spring-Repositorys verwenden, um CRUD-Dinge im laufenden Betrieb zu erledigen. Ich denke, ich generiere mit Spring eine ziemlich fortschrittliche REST-API für Datenbanken. Aber mit JSON ist ein ziemlich unerwarteter Overhead zu erwarten, sodass ich jedes einzelne Dokument auch mit generierten Abfragen verarbeiten muss.
Kensai
Sie können Hibernate OGM mit MongoDB verwenden, wenn JSON Ihr einziger Speicher ist.
Vlad Mihalcea
12

Bei Interesse können Sie JPA 2.1 @Convert/ @ConverterFunktionalität mit Hibernate verwenden. Sie müssten jedoch den JDBC-Treiber pgjdbc-ng verwenden . Auf diese Weise müssen Sie keine proprietären Erweiterungen, Dialekte und benutzerdefinierten Typen pro Feld verwenden.

@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
quelle
Das klingt nützlich - zwischen welchen Typen sollte es konvertieren, um JSON schreiben zu können? Ist es <MyCustomClass, String> oder ein anderer Typ?
Myrosia
Danke - habe gerade überprüft, dass es für mich funktioniert (JPA 2.1, Hibernate 4.3.10, pgjdbc-ng 0.5, Postgres 9.3)
Myrosia
Ist es möglich, dass es funktioniert, ohne @Column (columnDefinition = "json") im Feld anzugeben? Hibernate macht einen Varchar (255) ohne diese Definition.
Tfranckiewicz
Hibernate kann möglicherweise nicht wissen, welchen Spaltentyp Sie dort möchten, aber Sie bestehen darauf, dass Hibernate dafür verantwortlich ist, das Datenbankschema zu aktualisieren. Ich denke, es wird die Standardeinstellung ausgewählt.
vasily
3

Ich hatte ein ähnliches Problem mit Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: Keine Dialektzuordnung für JDBC-Typ: 1111) beim Ausführen nativer Abfragen (über EntityManager), bei denen JSON-Felder in der Projektion abgerufen wurden, obwohl die Entity-Klasse vorhanden war mit TypeDefs kommentiert. Die gleiche in HQL übersetzte Abfrage wurde problemlos ausgeführt. Um dies zu lösen, musste ich JsonPostgreSQLDialect folgendermaßen ändern:

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {

public JsonPostgreSQLDialect() {

    super();

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

Wobei myCustomType.StringJsonUserType der Klassenname der Klasse ist, die den json-Typ implementiert (von oben, Antwort von Tim Fulmer).

Balaban Mario
quelle
3

Ich habe viele Methoden ausprobiert, die ich im Internet gefunden habe. Die meisten funktionieren nicht, einige sind zu komplex. Das Folgende funktioniert für mich und ist viel einfacher, wenn Sie nicht die strengen Anforderungen für die PostgreSQL-Typvalidierung haben.

Machen Sie den PostgreSQL-JDBC-Zeichenfolgentyp wie nicht angegeben, wie z <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

TommyQu
quelle
Vielen Dank! Ich habe Ruhezustandstypen verwendet, aber das ist viel einfacher! Zu Ihrer Information
James
2

Es ist einfacher, dies zu tun, ohne eine Funktion mithilfe von zu erstellen 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
quelle
Danke, verwendet, um Varchar zu ltree zu gießen, funktioniert perfekt.
Vladimir M.
1

Ich bin darauf gestoßen und wollte keine Inhalte über eine Verbindungszeichenfolge aktivieren und implizite Konvertierungen zulassen. Zuerst habe ich versucht, @Type zu verwenden, aber da ich einen benutzerdefinierten Konverter zum Serialisieren / Deserialisieren einer Map zu / von JSON verwende, konnte ich keine @ Type-Annotation anwenden. Es stellte sich heraus, dass ich in meiner @ Column-Annotation nur columnDefinition = "json" angeben musste.

@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;
nenchev
quelle
3
Wo haben Sie diese HashMapConverter-Klasse definiert? Wie es aussieht.
Sandeep