programing

PostgreSQL JSON 열을 Hibernate 값 유형에 매핑

nasanasas 2020. 10. 29. 08:14
반응형

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() {
    }
}

간단한 해결 방법은 텍스트 열을 정의하는 것입니다.


PgJDBC 버그 # 265를 참조하십시오 .

PostgreSQL은 데이터 유형 변환에 대해 지나치게 엄격합니다. 그것은 암시 적 캐스팅하지 않습니다 text심지어 텍스트와 같은 같은 값으로 xmljson.

이 문제를 해결하는 가장 정확한 방법은 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에게 감사드립니다.


관심이 있으시다면 Hibernate 커스텀 사용자 유형을 가져 오는 몇 가지 코드 스 니펫이 있습니다. JAVA_OBJECT 포인터에 대한 Craig Ringer 덕분에 먼저 PostgreSQL 언어를 확장하여 json 유형에 대해 알려줍니다.

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


이 기사 에서 설명했듯이 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;

If you're using Hibernate 5 or later, then the JSON type is registered automatically by Postgre92Dialect.

Otherwise, you need to register it yourself:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

In case someone is interested, you can use JPA 2.1 @Convert / @Converter functionality with Hibernate. You would have to use the pgjdbc-ng JDBC driver though. This way you don't have to use any proprietary extensions, dialects and custom types per field.

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

I had a similar problem with Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: No Dialect mapping for JDBC type: 1111) when executing native queries (via EntityManager) that retrieved json fields in the projection although the Entity class has been annotated with TypeDefs. The same query translated in HQL was executed without any problem. To solve this I had to modify JsonPostgreSQLDialect this way:

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {

public JsonPostgreSQLDialect() {

    super();

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

Where myCustomType.StringJsonUserType is the class name of the class implementing the json type (from above, Tim Fulmer answer) .


I tried many methods I found on the Internet, most of them are not working, some of them are too complex. The below one works for me and is much more simple if you don't have that strict requirements for PostgreSQL type validation.

Make PostgreSQL jdbc string type as unspecified, like <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>


There is an easier to to do this which doesn't involve creating a function by using 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

참고URL : https://stackoverflow.com/questions/15974474/mapping-postgresql-json-column-to-hibernate-value-type

반응형