PostgreSQL JSON 열을 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

vladmihalcea.com/… 이 튜토리얼은 매우 간단합니다
SGuru

답변:


37

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에게 감사드립니다.


2
이 답변 의 결과 블로그 게시물 을 읽을 가치가 있습니다. 특히 주석 섹션은 이것의 위험성 (잘못된 json 허용)과 대체 / 우수한 솔루션을 강조합니다.
markdsievers dec

@markdsievers 감사합니다. 수정 된 솔루션으로 게시물을 업데이트했습니다.
Craig Ringer

@CraigRinger 문제 없습니다. 많은 PG / JPA / JDBC 공헌에 감사드립니다. 많은 사람들이 저에게 큰 도움이되었습니다.
markdsievers dec

1
@CraigRinger cstring어쨌든 변환 을 거치고 있으므로 단순히 사용할 수 CREATE CAST (text AS json) WITH INOUT없습니까?
Nick Barnes

@NickBarnes 그 솔루션도 나에게 완벽하게 작동했습니다 (그리고 내가 본 것에서 볼 때 유효하지 않은 JSON에서 실패합니다). 감사!
zeroDivisible 2014

76

관심이 있으시다면 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


2
걱정하지 마세요. 저는 코드와이 페이지를 제 앞에두고 그 이유를 알아 냈습니다. :) 이것이 Java 프로세스의 단점 일 수 있습니다. 어려운 문제에 대한 해결책을 통해 꽤 잘 생각하지만, 새로운 유형에 대한 일반 SPI와 같은 좋은 아이디어를 추가하는 것은 쉽지 않습니다. 우리는 구현 자 (이 경우 Hibernate)가 배치 된 모든 것을 남깁니다.
Tim Fulmer 2013

3
nullSafeGet에 대한 구현 코드에 문제가 있습니다. if (rs.wasNull ()) 대신 if (rs.getString (names [0]) == null)을 수행해야합니다. rs.wasNull ()이 무엇을하는지 잘 모르겠지만, 제 경우에는 내가 찾고 있던 값이 실제로 null이 아니었을 때 true를 반환하여 나를 태 웠습니다.
rtcarlson

1
트윗 담아 가기 그렇게해서 미안 해요. 위의 코드를 업데이트했습니다.
Tim Fulmer 2013 년

3
이 솔루션은 'No Dialect mapping for JDBC type : 1111'오류로 json 열에서 null을 검색하는 경우를 제외하고 Hibernate 4.2.7에서 잘 작동했습니다. 그러나 dialect 클래스에 다음 행을 추가하면 문제가 해결되었습니다. this.registerHibernateType (Types.OTHER, "StringJsonUserType");
oliverguenther 2013

7
링크 된 github-project 에 코드가 없습니다 ;-) BTW :이 코드를 재사용을위한 라이브러리로 사용하는 것이 유용하지 않을까요?
rü-

21

이것은 매우 일반적인 질문이므로 JPA 및 Hibernate를 사용할 때 JSON 열 유형을 매핑하는 가장 좋은 방법에 대한 매우 자세한 기사 를 작성하기로 결정했습니다 .

Maven 종속성

가장 먼저해야 할 일은 프로젝트 구성 파일 에 다음과 같은 Hibernate Types Maven 종속성 을 설정하는 것입니다 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;

나중에 최대 절전 모드 5를 사용하거나하는 경우, 그 JSON유형입니다 에 의해 자동으로 등록Postgre92Dialect .

그렇지 않으면 직접 등록해야합니다.

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

MySQL의 경우이 문서 에서 .NET Framework를 사용하여 JSON 객체를 매핑하는 방법을 확인할 수 있습니다 JsonStringType.


좋은 예이지만 MongoDB로 할 수있는 것처럼 네이티브 쿼리없이 데이터를 쿼리하기 위해 SpringData JPA 저장소와 같은 일반 DAO와 함께 사용할 수 있습니까? 이 사건에 대한 유효한 답이나 해결책을 찾지 못했습니다. 예, 데이터를 저장할 수 있으며 RDBMS에서 열을 필터링하여 검색 할 수 있지만 지금까지는 JSONB 열로 필터링 할 수 없습니다. 내가 틀 렸으면 좋겠고 그런 해결책이 있습니다.
kensai

그래 넌 할수있어. 그러나 SpringData JPA에서 지원하는 nativ 쿼리도 사용해야합니다.
Vlad Mihalcea

네이티브 쿼리 없이도 객체 메서드를 통해서만 갈 수 있다면 그것은 실제로 내 질문이었습니다. MongoDB 스타일의 @Document 주석과 같은 것입니다. 그래서 나는 이것이 PostgreSQL의 경우 멀지 않았고 유일한 해결책은 네이티브 쿼리-> nasty :-)라고 가정하지만 확인에 감사드립니다.
kensai

미래에 json의 필드 유형에 대한 테이블 및 문서 주석을 실제로 나타내는 엔티티와 같은 것을 보는 것이 좋을 것입니다. Spring 저장소를 사용하여 즉시 CRUD 작업을 수행 할 수 있습니다. Spring을 사용하여 데이터베이스에 대해 상당히 고급 REST API를 생성하고 있다고 생각하십시오. 그러나 JSON을 사용하면 예상치 못한 오버 헤드가 발생하므로 쿼리를 생성하여 모든 문서를 처리해야합니다.
kensai

JSON이 단일 저장소 인 경우 MongoDB와 함께 Hibernate OGM을 사용할 수 있습니다.
Vlad Mihalcea

12

누군가 관심이 있다면 Hibernate에서 JPA 2.1 @Convert/ @Converter기능을 사용할 수 있습니다 . 그래도 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;

이것은 유용하게 들립니다. JSON을 작성할 수 있으려면 어떤 유형을 변환해야합니까? <MyCustomClass, String> 또는 다른 유형입니까?
myrosia

감사합니다-방금 저에게 작동하는지 확인했습니다 (JPA 2.1, Hibernate 4.3.10, pgjdbc-ng 0.5, Postgres 9.3)
myrosia

필드에 @Column (columnDefinition = "json")을 지정하지 않고 작동하도록 할 수 있습니까? Hibernate는이 정의없이 varchar (255)를 만들고 있습니다.
tfranckiewicz

Hibernate는 당신이 원하는 컬럼 유형을 알 수 없지만, 데이터베이스 스키마를 업데이트하는 것은 Hibernate의 책임이라고 주장합니다. 그래서 나는 그것이 기본을 선택한다고 생각합니다.
vasily

3

Entity 클래스가 있지만 프로젝션에서 json 필드를 검색하는 네이티브 쿼리 (EntityManager를 통해)를 실행할 때 Postgres (javax.persistence.PersistenceException : org.hibernate.MappingException : No Dialect mapping for JDBC type : 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

나는 인터넷에서 찾은 많은 방법을 시도했지만 대부분이 작동하지 않으며 일부는 너무 복잡합니다. 아래는 저에게 효과적이며 PostgreSQL 유형 유효성 검사에 대한 엄격한 요구 사항이 없다면 훨씬 더 간단합니다.

PostgreSQL jdbc 문자열 유형을 지정되지 않은 것으로 만듭니다. <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>


감사합니다! 나는 최대 절전 모드를 사용하고 있었지만 이것이 훨씬 쉽습니다! 참고로 여기에이 매개 변수에 대한 문서가 있습니다. jdbc.postgresql.org/documentation/83/connect.html
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로 캐스트하면 완벽하게 작동합니다.
Vladimir M.

1

나는 이것을 실행하고 있었고 연결 문자열을 통해 물건을 활성화하고 암시 적 변환을 허용하고 싶지 않았습니다. 처음에는 @Type을 사용하려고했지만 사용자 지정 변환기를 사용하여 JSON과 맵을 직렬화 / 역 직렬화하기 때문에 @Type 주석을 적용 할 수 없습니다. 내 @Column 주석에 columnDefinition = "json"을 지정하기 만하면됩니다.

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

3
이 HashMapConverter 클래스를 어디에 정의 했습니까? 어떻게 생겼는지.
sandeep
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.