Java 将PostgreSQL JSON列映射到Hibernate实体属性

Java 将PostgreSQL JSON列映射到Hibernate实体属性,java,json,hibernate,postgresql,jpa,Java,Json,Hibernate,Postgresql,Jpa,在我的PostgreSQL DB(9.2)中有一个表,其列类型为JSON。我很难将此列映射到JPA2实体字段类型 我尝试使用字符串,但当我保存实体时,我得到一个异常,它无法将字符转换为JSON 处理JSON列时要使用的正确值类型是什么 @Entity public class MyEntity { private String jsonPayload; // this maps to a json column public MyEntity() { } } 一个简

在我的PostgreSQL DB(9.2)中有一个表,其列类型为JSON。我很难将此列映射到JPA2实体字段类型

我尝试使用字符串,但当我保存实体时,我得到一个异常,它无法将字符转换为JSON

处理JSON列时要使用的正确值类型是什么

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}
一个简单的解决方法是定义一个文本列

PostgreSQL对数据类型转换过于严格,令人烦恼。它不会隐式地将
文本
转换为类似文本的值,例如
xml
json

解决此问题的严格正确方法是编写一个自定义Hibernate映射类型,该类型使用JDBC
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指出这个问题。

PostgreSQL对数据类型转换过于严格,令人烦恼。它不会隐式地将
文本
转换为类似文本的值,例如
xml
json

解决此问题的严格正确方法是编写一个自定义Hibernate映射类型,该类型使用JDBC
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指出了这个问题。

如果您有兴趣,这里有一些代码片段,可以帮助您正确地使用Hibernate自定义用户类型。首先扩展PostgreSQL方言,告诉它json类型,感谢Craig Ringer提供的JAVA_对象指针:

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;
    }
}
然后对属性进行注释:

@Type(type = "StringJsonObject")
public String getBar() {
    return bar;
}
Hibernate将负责为您创建json类型的列,并处理来回映射。在用户类型实现中插入其他库以实现更高级的映射

如果有人想玩GitHub项目,这里有一个快速示例:


如果您感兴趣,这里有一些代码片段,可以让您正确地使用Hibernate自定义用户类型。首先扩展PostgreSQL方言,告诉它json类型,感谢Craig Ringer提供的JAVA_对象指针:

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;
    }
}
然后对属性进行注释:

@Type(type = "StringJsonObject")
public String getBar() {
    return bar;
}
Hibernate将负责为您创建json类型的列,并处理来回映射。在用户类型实现中插入其他库以实现更高级的映射

如果有人想玩GitHub项目,这里有一个快速示例:


如果有人感兴趣,您可以将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;
@javax.persistence.Converter
公共静态类MyCustomConverter实现AttributeConverter{
@凌驾
@NotNull
公共字符串convertToDatabaseColumn(@NotNull MuCustomClass myCustomObject){
...
}
@凌驾
@NotNull
public MuCustomClass convertToEntityAttribute(@NotNull字符串databaseDataAsJSONString){
...
}
}
...
@Convert(converter=MyCustomConverter.class)
私有MyCustomClass属性;

如果有人感兴趣,您可以在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;
@javax.persistence.Converter
公共静态类MyCustomConverter实现AttributeConverter{
@凌驾
@NotNull
公共字符串convertToDatabaseColumn(@NotNull MuCustomClass myCustomObject){
...
}
@凌驾
@NotNull
public MuCustomClass convertToEntityAttribute(@NotNull字符串databaseDataAsJSONString){
...
}
}
...
@Convert(converter=MyCustomConverter.class)
私有MyCustomClass属性;

我在执行本机查询(通过EntityManager)时遇到了与Postgres类似的问题(javax.persistence.PersistenceException:org.hibernate.MappingException:JDBC type:1111没有方言映射),该查询在投影中检索json字段,尽管实体类已用typedef注释。 在HQL中翻译的同一查询执行时没有出现任何问题。 为了解决这个问题,我必须以这种方式修改JSONPostgreSql方言:

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的答案)。

我在Postgres(javax.persistence.persistence)中遇到了类似的问题
@Convert(converter = HashMapConverter.class)
@Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;