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


81

我的PostgreSQL数据库(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

Answers:


37

请参阅PgJDBC错误#265

PostgreSQL过于严格,对数据类型转换非常严格。它text甚至不会隐式转换为类似文本的值,例如xmljson

解决此问题的严格正确方法是编写一个使用JDBCsetObject方法的自定义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

@markdsievers谢谢。我已使用更正后的解决方案更新了帖子。
克雷格·林格

@CraigRinger没问题。感谢您对PG / JPA / JDBC的贡献,其中许多对我有很大的帮助。
markdsievers 2013年

1
@CraigRinger既然您一直在进行cstring转换,那么您不能简单地使用CREATE CAST (text AS json) WITH INOUT吗?
尼克·巴恩斯

@NickBarnes那个解决方案也对我来说是完美的(从我所看到的情况来看,它应该在无效的JSON上失败,应该这样)。谢谢!
zeroDivisible 2014年

76

如果您有兴趣,请参考以下代码片段,以获取适当的Hibernate自定义用户类型。首先要扩展PostgreSQL方言以告知它有关json类型的信息,这要归功于Craig Ringer的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");
    }
}

接下来实现org.hibernate.usertype.UserType。下面的实现将String值映射到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。
蒂姆·富尔默

3
您的实现代码中存在nullSafeGet的问题。代替if(rs.wasNull()),您应该执行if(rs.getString(names [0])== null)。我不确定rs.wasNull()的功能,但是在我的情况下,当我寻找的值实际上不为null时,它通过返回true烧死了我。
rtcarlson 2013年

1
@rtcarlson不错!抱歉,您必须要经历。我已经更新了上面的代码。
Tim Fulmer 2013年

3
该解决方案与Hibernate 4.2.7配合得很好,除了从json列中检索null并出现错误“ JDBC类型没有No Dialect映射:1111”时。但是,将以下行添加到方言类即可解决此问题:this.registerHibernateType(Types.OTHER,“ StringJsonUserType”);
Oliverguenther

7
我在链接的github-project上看不到任何代码;-)顺便说一句:将此代码作为可重用的库是否有用?
–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;

如果您使用的是Hibernate 5或更高版本,则该JSON类型将由自动注册Postgre92Dialect

否则,您需要自己注册:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

对于MySQL,您可以查看本文以了解如何使用来映射JSON对象JsonStringType


很好的例子,但是可以将它与某些通用DAO一起使用吗,例如Spring Data JPA存储库来查询没有本机查询的数据,就像我们可以使用MongoDB一样?我没有找到任何有效的答案或解决方案。是的,我们可以存储数据,也可以通过过滤RDBMS中的列来检索数据,但到目前为止,我无法按JSONB颜色进行过滤。我希望我是错的,并且有这样的解决方案。
肯赛

是的你可以。但您还需要使用Spring Data JPA支持的nativ查询。
弗拉德·米哈尔切阿

我知道,这实际上是我的追求,如果我们可以不使用本机查询而仅通过对象方法进行查询。类似MongoDB风格的@Document注释。因此,我认为对于PostgreSQL来说还不是到目前为止,唯一的解决方案是本机查询-> nasty :-),但是感谢您的确认。
肯赛

希望以后能看到类似实体这样的实体,该实体真正表示json的字段类型上的表和文档注释,并且我可以使用Spring存储库动态地进行CRUD。认为我正在使用Spring为数据库生成相当高级的REST API。但是使用JSON后,我将面临相当大的意外开销,因此我还需要使用生成查询来处理每个文档。
肯赛

如果JSON是单个存储,则可以将Hibernate OGM与MongoDB一起使用。
弗拉德·米哈尔切亚

12

如果有人感兴趣,您可以将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;

听起来很有用-应该在哪种类型之间进行转换才能编写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 2015年

Hibernate可能无法知道您想要的列类型,但是您坚持认为更新数据库模式是Hibernate的责任。因此,我猜它选择了默认的一个。
vasily 2015年

3

当执行本机查询(通过EntityManager)检索投影中的json字段时,虽然Entity类已用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

我尝试了许多在Internet上发现的方法,其中大多数方法均无效,有些方法过于复杂。下面的代码对我有用,如果您对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,效果很好。
弗拉基米尔·

1

我遇到了这个问题,不想通过连接字符串启用内容,并允许隐式转换。最初,我尝试使用@Type,但是因为我使用的是自定义转换器将地图序列化/反序列化到JSON /从JSON反序列化,所以无法应用@Type批注。原来我只需要在@Column批注中指定columnDefinition =“ json”。

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

3
您在哪里定义了HashMapConverter类。看起来如何。
桑迪普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.