PostgreSQLJSON列をHibernateエンティティプロパティにマッピングする


81

PostgreSQL DB(9.2)にJSON型の列を持つテーブルがあります。この列をJPA2エンティティフィールドタイプにマップするのに苦労しています。

Stringを使おうとしましたが、エンティティを保存すると、文字の変化をJSONに変換できないという例外が発生します。

JSON列を処理するときに使用する正しい値のタイプは何ですか?

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

簡単な回避策は、テキスト列を定義することです。


2
私はこれが少し古いことを知っていますが、同様の質問については私の答えstackoverflow.com/a/26126168/1535995を見てください
Sasa7812 2014

vladmihalcea.com/...このチュートリアルでは、非常に簡単です
SGuru

回答:


37

PgJDBCバグ#265を参照してください

PostgreSQLは、データ型の変換に関して過度に、煩わしいほど厳格です。text次のようなテキストのような値にも暗黙的にキャストされませんxmljson

この問題を解決するための厳密に正しい方法は、JDBCを使用するカスタムHibernateマッピングタイプを作成することです。 setObjectメソッドすることです。これはかなり面倒な場合があるため、より弱いキャストを作成してPostgreSQLの厳密さを緩和したい場合があります。

コメントとこのブログ投稿で@markdsieversが指摘しているように、この回答の元のソリューションはJSON検証をバイパスします。だから、それは本当にあなたが望むものではありません。書く方が安全です:

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 明示的に指示されることなく変換できることをPostgreSQLに通知し、次のような機能を可能にします。

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

問題を指摘してくれた@markdsieversに感謝します。


2
この回答の結果のブログ投稿を読む価値があります。特にコメントセクションでは、これの危険性(無効なjsonを許可)と代替/優れたソリューションを強調しています。
markdsievers 2013

@markdsieversありがとうございます。修正された解決策で投稿を更新しました。
クレイグリンガー

@CraigRinger問題ありません。PG / JPA / JDBCに多大な貢献をしていただき、ありがとうございます。多くの人が私を大いに助けてくれました。
markdsievers 2013

1
@CraigRingercstringとにかく変換​​を行っているので、単純に使用できませんでしたCREATE CAST (text AS json) WITH INOUTか?
ニックバーンズ

@NickBarnesそのソリューションも私にとっては完璧に機能しました(そして私が見たところ、無効なJSONでは失敗します)。ありがとう!
zeroDivisible 2014

76

興味がある場合は、Hibernateカスタムユーザータイプを配置するためのコードスニペットをいくつか示します。まず、PostgreSQLダイアレクトを拡張してjsonタイプについて通知します。これは、JAVA_OBJECTポインターのCraigRingerのおかげです。

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

次に、org.hibernate.usertype.UserTypeを実装します。以下の実装は、文字列値をjsonデータベースタイプにマップし、その逆も同様です。文字列はJavaでは不変であることを忘れないでください。より複雑な実装を使用して、カスタムJavaBeanをデータベースに格納されているJSONにマップすることもできます。

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

残っているのは、エンティティに注釈を付けることだけです。エンティティのクラス宣言に次のようなものを配置します。

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

次に、プロパティに注釈を付けます。

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

Hibernateは、jsonタイプの列の作成を処理し、マッピングを前後に処理します。より高度なマッピングのために、ユーザータイプの実装に追加のライブラリを挿入します。

誰かがそれをいじりたい場合は、GitHubプロジェクトの簡単なサンプルを次に示します。

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


2
心配はいりません。コードとこのページが目の前に表示され、その理由がわかりました:)これはJavaプロセスの欠点かもしれません。難しい問題の解決策についてはかなりよく考えられていますが、新しいタイプの汎用SPIのような良いアイデアを追加するのは簡単ではありません。実装者(この場合はHibernate)が配置されているものは何でも残されています。
Tim Fulmer 2013

3
nullSafeGetの実装コードに問題があります。if(rs.wasNull())の代わりに、if(rs.getString(names [0])== null)を実行する必要があります。rs.wasNull()が何をするのかはわかりませんが、私の場合、探していた値が実際にはnullではなかったのに、trueを返すことでやけどを負いました。
rtcarlson 2013

1
@rtcarlsonいいキャッチ!申し訳ありませんが、それを通過する必要がありました。上記のコードを更新しました。
Tim Fulmer

3
このソリューションは、「JDBCタイプの方言マッピングがありません:1111」というエラーでjson列からnullを取得する場合を除いて、Hibernate4.2.7でうまく機能しました。ただし、方言クラスに次の行を追加すると修正されました。this.registerHibernateType(Types.OTHER、 "StringJsonUserType");
Oliverguenther 2013年

7
リンクされたgithub-projectにコードが表示されません;-)ところで:このコードを再利用用のライブラリとして持つと便利ではないでしょうか?
のRu-

21

Mavenの依存関係

最初に行う必要があるのは、プロジェクト構成ファイルに次のHibernate TypesMaven依存関係をpom.xml設定することです。

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

ドメインモデル

ここで、PostgreSQLを使用している場合は、次のようJsonBinaryTypeに、クラスレベルまたはpackage-info.javaパッケージレベル記述子で宣言する必要があります。

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

また、エンティティマッピングは次のようになります。

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

Hibernate 5以降を使用している場合、JSONタイプはによって自動的に登録されます。Postgre92Dialect

それ以外の場合は、自分で登録する必要があります。

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

MySQLの場合、を使用してJSONオブジェクトをマップできますJsonStringType


良い例ですが、これをSpring Data JPAリポジトリなどの一般的なDAOで使用して、MongoDBで実行できるようなネイティブクエリなしでデータをクエリできますか?このケースに対する有効な答えや解決策は見つかりませんでした。はい、データを保存でき、RDBMSで列をフィルタリングすることでデータを取得できますが、これまでのところJSONB列でフィルタリングすることはできません。私は間違っていて、そのような解決策があるといいのですが。
kensai 2017

はい、できます。ただし、Spring DataJPAでもサポートされているnativクエリを使用する必要があります。
Vlad Mihalcea 2017

ネイティブクエリなしで、オブジェクトメソッドを介して実行できるのであれば、それが実際の私の質問でした。MongoDBスタイルの@Documentアノテーションのようなもの。したがって、これはPostgreSQLの場合はそれほど遠くなく、唯一の解決策はネイティブクエリ->厄介な:-)であると思いますが、確認していただきありがとうございます。
kensai 2017

将来的には、jsonのフィールドタイプでテーブルとドキュメントの注釈を実際に表すエンティティのようなものを見るとよいでしょう。Springリポジトリを使用して、その場でCRUDを実行できます。Springを使用してデータベース用の非常に高度なRESTAPIを生成していると思います。しかし、JSONを配置すると、まったく予期しないオーバーヘッドに直面するため、生成クエリを使用してすべてのドキュメントを処理する必要があります。
kensai 2017

JSONが単一のストアである場合は、MongoDBでHibernateOGMを使用できます。
Vlad Mihalcea 2017

12

誰かが興味を持っている場合は、HibernateでJPA 2.1 @Convert/@Converter機能を使用できます。ただし、pgjdbc-ngJDBCドライバーを使用する必要があります。このように、フィールドごとに独自の拡張機能、方言、カスタムタイプを使用する必要はありません。

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

これは便利に聞こえます-JSONを記述できるようにするには、どのタイプに変換する必要がありますか?<MyCustomClass、String>または他のタイプですか?
myrosia 2015

ありがとう-それが私のために動作することを確認しました(JPA 2.1、Hibernate 4.3.10、pgjdbc-ng 0.5、Postgres 9.3)
myrosia 2015

フィールドで@Column(columnDefinition = "json")を指定せずに機能させることは可能ですか?Hibernateはこの定義なしでvarchar(255)を作成しています。
tfranckiewicz 2015

Hibernateは、必要な列タイプを認識できない可能性がありますが、データベーススキーマを更新するのはHibernateの責任であると主張します。だから私はそれがデフォルトのものを選ぶと思います。
vasily 2015

3

エンティティクラスがTypeDefsで注釈が付けられています。HQLで変換された同じクエリが問題なく実行されました。これを解決するには、JsonPostgreSQLDialectを次のように変更する必要がありました。

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {

public JsonPostgreSQLDialect() {

    super();

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

myCustomType.StringJsonUserTypeは、jsonタイプを実装するクラスのクラス名です(上から、Tim Fulmerの回答)。


3

私はインターネットで見つけた多くの方法を試しましたが、それらのほとんどは機能せず、いくつかは複雑すぎます。以下のものは私にとってはうまくいきますが、PostgreSQLの型の検証に厳密な要件がない場合は、はるかに簡単です。

PostgreSQLjdbc文字列型を指定されていないものにします。 <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>


ありがとうございました!私はHibernateタイプを使用していましたが、これははるかに簡単です!参考までに
James

2

これを行うには、を使用して関数を作成する必要がない方が簡単です。 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

おかげで、これを使用してvarcharをltreeにキャストし、完全に機能します。
ウラジミール

1

私はこれに遭遇し、接続文字列を介してものを有効にし、暗黙の変換を許可したくありませんでした。最初は@Typeを使おうとしましたが、カスタムコンバーターを使用してマップをJSONとの間でシリアル化/逆シリアル化するため、@ Typeアノテーションを適用できませんでした。@ColumnアノテーションでcolumnDefinition = "json"を指定する必要があることがわかりました。

@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;

3
このHashMapConverterクラスをどこで定義しましたか。それがどのように見えるか。
サンディープ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.