Ánh xạ cột PostgreSQL JSON thành thuộc tính thực thể Hibernate


81

Tôi có một bảng với một cột kiểu JSON trong PostgreSQL DB (9.2) của tôi. Tôi gặp khó khăn khi ánh xạ cột này sang loại trường Thực thể JPA2.

Tôi đã cố gắng sử dụng Chuỗi nhưng khi tôi lưu thực thể, tôi nhận được một ngoại lệ rằng nó không thể chuyển đổi ký tự thay đổi thành JSON.

Loại giá trị chính xác để sử dụng khi xử lý cột JSON là gì?

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

Một giải pháp đơn giản sẽ là xác định một cột văn bản.


2
Tôi biết đây là một chút cũ, nhưng có một cái nhìn vào câu trả lời của tôi stackoverflow.com/a/26126168/1535995 cho câu hỏi tương tự
Sasa7812

vladmihalcea.com/… hướng dẫn này khá đơn giản
SGuru

Câu trả lời:


37

Xem lỗi PgJDBC # 265 .

PostgreSQL nghiêm ngặt quá mức, gây khó chịu về chuyển đổi kiểu dữ liệu. Nó sẽ không truyền ngầm textngay cả đến các giá trị giống văn bản như xmljson.

Cách chính xác để giải quyết vấn đề này là viết một kiểu ánh xạ Hibernate tùy chỉnh sử dụng setObjectphương thức JDBC . Điều này có thể hơi phức tạp, vì vậy bạn có thể chỉ muốn làm cho PostgreSQL bớt nghiêm ngặt hơn bằng cách tạo một dàn diễn viên yếu hơn.

Theo ghi nhận của @markdsievers trong nhận xét và bài đăng trên blog này , giải pháp ban đầu trong câu trả lời này bỏ qua xác thực JSON. Vì vậy, nó không thực sự như những gì bạn muốn. Sẽ an toàn hơn khi viết:

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 nói với PostgreSQL rằng nó có thể chuyển đổi mà không cần được yêu cầu rõ ràng, cho phép những thứ như thế này hoạt động:

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

Cảm ơn @markdsievers đã chỉ ra vấn đề.


2
Đáng đọc bài đăng trên blog kết quả của câu trả lời này. Inparticular, phần bình luận nêu bật sự nguy hiểm của điều này (cho phép json không hợp lệ) và giải pháp thay thế / ưu việt.
markdsievers

@markdsievers Thankyou. Tôi đã cập nhật bài đăng với một giải pháp đã sửa chữa.
Craig Ringer

@CraigRinger Không sao. Cảm ơn bạn đã đóng góp nhiều cho PG / JPA / JDBC, nhiều người đã hỗ trợ tôi rất nhiều.
markdsievers

1
@CraigRinger Vì bạn vẫn đang thực hiện cstringchuyển đổi, bạn không thể sử dụng đơn giản CREATE CAST (text AS json) WITH INOUT?
Nick Barnes

@NickBarnes giải pháp đó cũng hoạt động hoàn hảo đối với tôi (và từ những gì tôi đã thấy, nó không thành công trên JSON không hợp lệ, vì nó nên làm). Cảm ơn!
zeroDivible

76

Nếu bạn quan tâm, đây là một vài đoạn mã để đưa loại người dùng tùy chỉnh Hibernate vào đúng vị trí. Đầu tiên hãy mở rộng phương ngữ PostgreSQL để cho nó biết về kiểu json, nhờ Craig Ringer cho con trỏ JAVA_OBJECT:

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

Tiếp theo triển khai org.hibernate.usertype.UserType. Việc triển khai bên dưới ánh xạ các giá trị Chuỗi sang kiểu cơ sở dữ liệu json và ngược lại. Hãy nhớ rằng các chuỗi là bất biến trong Java. Một triển khai phức tạp hơn cũng có thể được sử dụng để ánh xạ các hạt Java tùy chỉnh sang JSON được lưu trữ trong cơ sở dữ liệu.

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

Bây giờ tất cả những gì còn lại là chú thích các thực thể. Đặt một cái gì đó như thế này tại khai báo lớp của thực thể:

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

Sau đó, chú thích thuộc tính:

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

Hibernate sẽ đảm nhận việc tạo cột với kiểu json cho bạn và xử lý ánh xạ qua lại. Đưa các thư viện bổ sung vào triển khai kiểu người dùng để ánh xạ nâng cao hơn.

Dưới đây là một dự án GitHub mẫu nhanh nếu có ai muốn chơi với nó:

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


2
Đừng lo lắng, tôi đã kết thúc với mã và trang này trước mặt tôi và tìm ra lý do tại sao không :) Đó có thể là nhược điểm của quy trình Java. Chúng tôi nhận được một số suy nghĩ khá tốt thông qua các giải pháp cho các vấn đề khó khăn, nhưng không dễ dàng để đi vào và thêm một ý tưởng hay như SPI chung cho các loại mới. Chúng tôi còn lại bất cứ thứ gì mà những người triển khai, Hibernate trong trường hợp này, đã đặt.
Tim Fulmer

3
có vấn đề trong mã triển khai của bạn cho nullSafeGet. Thay vì if (rs.wasNull ()), bạn nên làm if (rs.getString (names [0]) == null). Tôi không chắc rs.wasNull () làm gì, nhưng trong trường hợp của tôi, nó đã đốt cháy tôi bằng cách trả về true, khi giá trị tôi đang tìm kiếm thực tế không phải là null.
rtcarlson

1
@rtcarlson Bắt hay lắm! Xin lỗi bạn đã phải trải qua điều đó. Tôi đã cập nhật mã ở trên.
Tim Fulmer 20/09/13

3
Giải pháp này hoạt động hiệu quả với Hibernate 4.2.7 ngoại trừ khi truy xuất null từ các cột json với lỗi 'Không có ánh xạ phương thức cho kiểu JDBC: 1111'. Tuy nhiên, việc thêm dòng sau vào lớp phương ngữ đã sửa nó: this.registerHibernateType (Types.OTHER, "StringJsonUserType");
oliverguenther

7
Tôi không thấy bất kỳ mã nào trên github-project được liên kết ;-) BTW: Sẽ không hữu ích nếu sử dụng lại mã này như một thư viện?
rü-

21

Đây là một câu hỏi rất phổ biến, vì vậy tôi quyết định viết một bài rất chi tiết , về cách tốt nhất để ánh xạ các loại cột JSON khi sử dụng JPA và Hibernate.

Maven phụ thuộc

Điều đầu tiên bạn cần làm là thiết lập phụ thuộc Hibernate Types Maven sau trong pom.xmltệp cấu hình dự án của bạn :

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

Mô hình miền

Bây giờ, nếu bạn đang sử dụng PostgreSQL, bạn cần phải khai báo JsonBinaryTypeở cấp độ lớp hoặc trong bộ mô tả cấp độ gói package-info.java , như sau:

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

Và, ánh xạ thực thể sẽ trông như thế này:

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

Nếu bạn đang sử dụng Hibernate 5 trở lên, thì JSONkiểu được đăng ký tự động bởiPostgre92Dialect .

Nếu không, bạn cần phải tự đăng ký:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

Đối với MySQL, bạn có thể xem bài viết này để xem cách bạn có thể ánh xạ các đối tượng JSON bằng cách sử dụng JsonStringType.


Ví dụ đẹp, nhưng điều này có thể được sử dụng với một số DAO chung, như kho lưu trữ Spring Data JPA để truy vấn dữ liệu mà không cần truy vấn gốc như chúng ta có thể làm với MongoDB không? Tôi không tìm thấy bất kỳ câu trả lời hoặc giải pháp hợp lệ nào cho trường hợp này. Có, chúng tôi có thể lưu trữ dữ liệu và chúng tôi có thể truy xuất chúng bằng cách lọc các cột trong RDBMS, nhưng tôi không thể lọc theo các cột JSONB cho đến nay. Tôi ước tôi sai và có giải pháp như vậy.
kensai

Có, bạn có thể. Nhưng bạn cũng cần sử dụng các truy vấn nativ cũng được hỗ trợ bởi Spring Data JPA.
Vlad Mihalcea

Tôi hiểu rồi, đó thực sự là câu hỏi của tôi, nếu chúng ta có thể đi mà không cần truy vấn gốc mà chỉ thông qua các phương thức đối tượng. Một cái gì đó giống như chú thích @Document cho kiểu MongoDB. Vì vậy, tôi cho rằng điều này không quá xa trong trường hợp của PostgreSQL và giải pháp duy nhất là truy vấn gốc -> nasty :-), nhưng cảm ơn đã xác nhận.
kensai

Sẽ rất tốt nếu thấy trong tương lai một thứ gì đó giống như entity thực sự đại diện cho chú thích bảng và tài liệu trên loại trường của json và tôi có thể sử dụng kho lưu trữ Spring để thực hiện các công việc CRUD một cách nhanh chóng. Hãy nghĩ rằng tôi đang tạo API REST khá nâng cao cho cơ sở dữ liệu với Spring. Nhưng với JSON tại chỗ, tôi đang phải đối mặt với chi phí khá bất ngờ vì vậy tôi cũng sẽ cần xử lý từng tài liệu đơn lẻ với các truy vấn tạo.
kensai

Bạn có thể sử dụng Hibernate OGM với MongoDB nếu JSON là cửa hàng duy nhất của bạn.
Vlad Mihalcea

12

Trong trường hợp ai đó quan tâm, bạn có thể sử dụng JPA 2.1 @Convert/ @Converterchức năng với Hibernate. Mặc dù vậy, bạn sẽ phải sử dụng trình điều khiển JDBC pgjdbc-ng . Bằng cách này, bạn không phải sử dụng bất kỳ tiện ích mở rộng, phương ngữ và loại tùy chỉnh độc quyền nào cho mỗi trường.

@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ều này nghe có vẻ hữu ích - nó nên chuyển đổi giữa những loại nào để có thể viết JSON? Đó là <MyCustomClass, String> hay một số loại khác?
myrosia,

Cảm ơn - chỉ xác nhận rằng nó làm việc cho tôi (JPA 2.1, Hibernate 4.3.10, pgjdbc-ng 0,5, Postgres 9.3)
myrosia

Có thể làm cho nó hoạt động mà không cần chỉ định @Column (columnDefinition = "json") trên trường không? Hibernate đang tạo một varchar (255) không có định nghĩa này.
tfranckiewicz

Hibernate không thể biết loại cột bạn muốn ở đó, nhưng bạn nhấn mạnh rằng trách nhiệm của Hibernate là cập nhật lược đồ cơ sở dữ liệu. Vì vậy, tôi đoán nó chọn cái mặc định.
vasily

3

Tôi đã gặp sự cố tương tự với Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: Không ánh xạ phương thức cho loại JDBC: 1111) khi thực thi các truy vấn gốc (thông qua EntityManager) đã truy xuất các trường json trong phép chiếu mặc dù lớp Thực thể đã được được chú thích bằng TypeDefs. Truy vấn tương tự được dịch trong HQL đã được thực thi mà không gặp bất kỳ sự cố nào. Để giải quyết vấn đề này, tôi đã phải sửa đổi JsonPostgreSQLDialect theo cách này:

public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {

public JsonPostgreSQLDialect() {

    super();

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

Trong đó myCustomType.StringJsonUserType là tên lớp của lớp thực hiện kiểu json (từ trên, câu trả lời của Tim Fulmer).


3

Tôi đã thử nhiều phương pháp tôi tìm thấy trên Internet, hầu hết chúng đều không hoạt động, một số trong số chúng quá phức tạp. Cái dưới đây phù hợp với tôi và đơn giản hơn nhiều nếu bạn không có yêu cầu nghiêm ngặt về xác thực kiểu PostgreSQL.

Đặt loại chuỗi PostgreSQL jdbc là không xác định, như <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>


Cảm ơn bạn! Tôi đã sử dụng kiểu ngủ đông nhưng cách này dễ hơn! FYI đây là tài liệu về thông số này jdbc.postgresql.org/documentation/83/connect.html
James

2

Có một cách dễ dàng hơn để làm điều này mà không liên quan đến việc tạo một hàm bằng cách sử dụng 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

Cảm ơn, đã sử dụng cái này để truyền varchar sang ltree, hoạt động hoàn hảo.
Vladimir M.

1

Tôi đang gặp phải vấn đề này và không muốn kích hoạt nội dung thông qua chuỗi kết nối và cho phép chuyển đổi ngầm. Lúc đầu, tôi đã cố gắng sử dụng @Type, nhưng vì tôi đang sử dụng trình chuyển đổi tùy chỉnh để tuần tự hóa / giải mã hóa Bản đồ đến / từ JSON, tôi không thể áp dụng chú thích @Type. Hóa ra tôi chỉ cần chỉ định columnDefinition = "json" trong chú thích @Column của mình.

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

3
Bạn đã định nghĩa lớp HashMapConverter này ở đâu. Nó trông như thế nào.
sandeep
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.