web-dev-qa-db-ja.com

PostgreSQL JSON列のHibernate値型へのマッピング

PostgreSQL DB(9.2)にJSON型の列を持つテーブルがあります。この列をJPA2エンティティフィールドタイプにマッピングするのは困難です。

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

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

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

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

71
Ümit

PgJDBCバグ#265を参照

PostgreSQLは、データ型の変換について非常に面倒です。 textxmlなどのテキストのような値であっても、暗黙的にjsonをキャストしません。

この問題を解決する厳密に正しい方法は、JDBC setObjectメソッドを使用するカスタムHibernateマッピングタイプを記述することです。これはかなり面倒な作業になる可能性があるため、弱いキャストを作成して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に感謝します。

36
Craig Ringer

興味がある場合は、Hibernateカスタムユーザータイプを適切に取得するためのコードスニペットをいくつか紹介します。 Java_OBJECTポインタのCraig Ringerに感謝します。

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では文字列は不変であることに注意してください。より複雑な実装を使用して、カスタムJava Beanをデータベースに格納されている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

74
Tim Fulmer

この記事 で説明したように、Hibernateを使用してJSONオブジェクトを永続化するのは非常に簡単です。

これらすべてのタイプを手動で作成する必要はありません。次の依存関係を使用して、Maven Centralから簡単に取得できます。

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

詳細については、 hibernate-typesオープンソースプロジェクト をご覧ください。

それでは、すべての仕組みを説明します。

記事 PostgreSQLとMySQLの両方でJSONオブジェクトをマップする方法について書いた。

PostgreSQLの場合、JSONオブジェクトをバイナリ形式で送信する必要があります。

public class JsonBinaryType
    extends AbstractSingleColumnStandardBasicType<Object> 
    implements DynamicParameterizedType {

    public JsonBinaryType() {
        super( 
            JsonBinarySqlTypeDescriptor.INSTANCE, 
            new JsonTypeDescriptor()
        );
    }

    public String getName() {
        return "jsonb";
    }

    @Override
    public void setParameterValues(Properties parameters) {
        ((JsonTypeDescriptor) getJavaTypeDescriptor())
            .setParameterValues(parameters);
    }

}

JsonBinarySqlTypeDescriptorは次のようになります。

public class JsonBinarySqlTypeDescriptor
    extends AbstractJsonSqlTypeDescriptor {

    public static final JsonBinarySqlTypeDescriptor INSTANCE = 
        new JsonBinarySqlTypeDescriptor();

    @Override
    public <X> ValueBinder<X> getBinder(
        final JavaTypeDescriptor<X> javaTypeDescriptor) {
        return new BasicBinder<X>(javaTypeDescriptor, this) {
            @Override
            protected void doBind(
                PreparedStatement st, 
                X value, 
                int index, 
                WrapperOptions options) throws SQLException {
                st.setObject(index, 
                    javaTypeDescriptor.unwrap(
                        value, JsonNode.class, options), getSqlType()
                );
            }

            @Override
            protected void doBind(
                CallableStatement st, 
                X value, 
                String name, 
                WrapperOptions options)
                    throws SQLException {
                st.setObject(name, 
                    javaTypeDescriptor.unwrap(
                        value, JsonNode.class, options), getSqlType()
                );
            }
        };
    }
}

JsonTypeDescriptorは次のようになります。

public class JsonTypeDescriptor
        extends AbstractTypeDescriptor<Object> 
        implements DynamicParameterizedType {

    private Class<?> jsonObjectClass;

    @Override
    public void setParameterValues(Properties parameters) {
        jsonObjectClass = ( (ParameterType) parameters.get( PARAMETER_TYPE ) )
            .getReturnedClass();

    }

    public JsonTypeDescriptor() {
        super( Object.class, new MutableMutabilityPlan<Object>() {
            @Override
            protected Object deepCopyNotNull(Object value) {
                return JacksonUtil.clone(value);
            }
        });
    }

    @Override
    public boolean areEqual(Object one, Object another) {
        if ( one == another ) {
            return true;
        }
        if ( one == null || another == null ) {
            return false;
        }
        return JacksonUtil.toJsonNode(JacksonUtil.toString(one)).equals(
                JacksonUtil.toJsonNode(JacksonUtil.toString(another)));
    }

    @Override
    public String toString(Object value) {
        return JacksonUtil.toString(value);
    }

    @Override
    public Object fromString(String string) {
        return JacksonUtil.fromString(string, jsonObjectClass);
    }

    @SuppressWarnings({ "unchecked" })
    @Override
    public <X> X unwrap(Object value, Class<X> type, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( String.class.isAssignableFrom( type ) ) {
            return (X) toString(value);
        }
        if ( Object.class.isAssignableFrom( type ) ) {
            return (X) JacksonUtil.toJsonNode(toString(value));
        }
        throw unknownUnwrap( type );
    }

    @Override
    public <X> Object wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        return fromString(value.toString());
    }

}

次に、クラスレベルまたは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" );
    }
}
13
Vlad Mihalcea

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

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

Entityクラスが取得されているにもかかわらず、プロジェクションでjsonフィールドを取得するネイティブクエリ(EntityManagerを介して)を実行すると、Postgres(javax.persistence.PersistenceException:org.hibernate.MappingException:JDBCタイプの方言マッピングなし:1111)で同様の問題が発生しました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
Balaban Mario

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

<connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>のように、PostgreSQLのjdbc文字列タイプを未指定にします

2
TommyQu

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
1
Evan Carroll