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

Posted

技术标签:

【中文标题】将 PostgreSQL JSON 列映射到 Hibernate 实体属性【英文标题】:Mapping PostgreSQL JSON column to a Hibernate entity property 【发布时间】:2013-04-05 03:57:05 【问题描述】:

我的 PostgreSQL 数据库 (9.2) 中有一个包含 JSON 类型列的表。我很难将此列映射到 JPA2 实体字段类型。

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

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

@Entity
public class MyEntity 

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

    public MyEntity() 
    

一个简单的解决方法是定义一个文本列。

【问题讨论】:

我知道这有点老了,但是看看我的回答 ***.com/a/26126168/1535995 类似的问题 vladmihalcea.com/… 这个教程很简单 【参考方案1】:

如果您有兴趣,这里有一些代码 sn-ps 用于获取 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

【讨论】:

别担心,伙计们,我最终得到了代码和这个页面,并想出了为什么不这样做:) 这可能是 Java 进程的缺点。我们通过对棘手问题的解决方案获得了一些经过深思熟虑的解决方案,但是要进入并为新类型添加一个像通用 SPI 这样的好主意并不容易。我们只剩下实现者(在这种情况下为 Hibernate)实施的任何东西。 您的 nullSafeGet 实现代码有问题。而不是 if(rs.wasNull()) 你应该做 if(rs.getString(names[0]) == null)。我不确定 rs.wasNull() 做了什么,但在我的情况下,当我正在寻找的值实际上不是 null 时,它通过返回 true 让我很伤心。 @rtcarlson 不错的收获!对不起,你必须经历那个。我已经更新了上面的代码。 此解决方案与 Hibernate 4.2.7 配合得很好,除非从 json 列中检索 null 时出现错误“没有 JDBC 类型的方言映射:1111”。但是,将以下行添加到方言类修复了它: this.registerHibernateType(Types.OTHER, "StringJsonUserType"); 我在链接的github-project 上看不到任何代码 ;-) BTW:将这些代码作为库以供重用不是很有用吗?【参考方案2】:

See PgJDBC bug #265.

PostgreSQL 对数据类型转换过于严格,令人讨厌。它不会将text 隐式转换为类似文本的值,例如xmljson

解决这个问题的严格正确的方法是编写一个使用 JDBC setObject 方法的自定义 Hibernate 映射类型。这可能有点麻烦,因此您可能只想通过创建较弱的强制转换来降低 PostgreSQL 的严格性。

正如 cmets 中的 @markdsievers 和 this blog post 所指出的,此答案中的原始解决方案绕过了 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 指出问题。

【讨论】:

值得一读这个答案的结果blog post。特别是评论部分强调了这种危险(允许无效的 json)和替代/优越的解决方案。 @markdsievers 谢谢。我已经用更正的解决方案更新了帖子。 @CraigRinger 没问题。感谢您多产的 PG / JPA / JDBC 贡献,许多对我有很大帮助。 @CraigRinger 既然你正在经历cstring的转换,你不能简单地使用CREATE CAST (text AS json) WITH INOUT吗? @NickBarnes 该解决方案对我也很有效(据我所见,它在无效的 JSON 上失败,因为它应该)。谢谢!【参考方案3】:

Maven 依赖

你需要做的第一件事是在你的项目pom.xml配置文件中设置如下Hibernate TypesMaven依赖:

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

领域模型

现在,您需要在类级别或 package-info.java 包级别描述符中声明 JsonType,如下所示:

@TypeDef(name = "json", typeClass = JsonType.class)

而且,实体映射将如下所示:

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

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

否则需要自己注册:

public class PostgreSQLDialect extends PostgreSQL91Dialect 

    public PostgreSQL92Dialect() 
        super();
        this.registerColumnType( Types.JAVA_OBJECT, "jsonb" );
    

【讨论】:

很好的例子,但是这可以与一些通用的 DAO 一起使用,比如 Spring Data JPA 存储库来查询数据,而无需像 MongoDB 那样做本机查询吗?我没有找到任何有效的答案或解决方案。是的,我们可以存储数据,我们可以通过过滤 RDBMS 中的列来检索它们,但到目前为止我无法通过 JSONB 列进行过滤。我希望我错了,有这样的解决方案。 是的,你可以。但是您也需要使用 Spring Data JPA 支持的 nativ 查询。 我明白了,这实际上是我的任务,如果我们可以不使用本机查询,而只是通过对象方法。类似于 MongoDB 样式的 @Document 注释。所以我认为这不是 PostgreSQL 的情况,唯一的解决方案是本地查询 -> 讨厌 :-),但感谢您的确认。 很高兴将来能看到像实体这样的东西,它真正代表 json 字段类型的表和文档注释,我可以使用 Spring 存储库即时执行 CRUD 工作。想想我正在使用 Spring 为数据库生成相当高级的 REST API。但是使用 JSON 后,我面临着非常意想不到的开销,因此我还需要使用生成查询来处理每个文档。 如果 JSON 是您的单一存储,您可以将 Hibernate OGM 与 MongoDB 结合使用。【参考方案4】:

如果有人感兴趣,您可以在 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?是 还是其他类型? 谢谢 - 刚刚验证它对我有用(JPA 2.1、Hibernate 4.3.10、pgjdbc-ng 0.5、Postgres 9.3) 是否可以在不指定 @Column(columnDefinition = "json") 的情况下使其工作? Hibernate 正在制作一个没有此定义的 varchar(255)。 Hibernate 不可能知道您想要的列类型,但您坚持认为更新数据库模式是 Hibernate 的责任。所以我猜它会选择默认的。【参考方案5】:

我尝试了很多在网上找到的方法,大部分都不行,有些方法太复杂了。如果您对 PostgreSQL 类型验证没有那么严格的要求,下面的一个对我有用,而且会简单得多。

将 PostgreSQL jdbc 字符串类型设为未指定,例如 <connection-url> jdbc:postgresql://localhost:test?stringtype=‌​unspecified </connect‌​ion-url>

【讨论】:

谢谢!我使用的是休眠类型,但这更容易!仅供参考,这里是有关此参数的文档 jdbc.postgresql.org/documentation/83/connect.html【参考方案6】:

在执行本机查询(通过 EntityManager)时,虽然实体类已使用 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 回答)。

【讨论】:

【参考方案7】:

有一个更容易做到这一点,它不涉及使用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,效果很好。【参考方案8】:

我遇到了这个问题,不想通过连接字符串启用东西,并允许隐式转换。起初我尝试使用@Type,但因为我使用自定义转换器来序列化/反序列化映射到 JSON 的映射,所以我无法应用 @Type 注释。原来我只需要在 @Column 注释中指定 columnDefinition = "json" 。

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

【讨论】:

你在哪里定义了这个 HashMapConverter 类。它的样子。【参考方案9】:

上述所有解决方案都不适合我。最后我利用原生查询来插入数据。

步骤 -1 创建一个抽象类 AbstractEntity 将实现 Persistable 带有注释@MappedSuperclass(javax.persistence 的一部分) 步骤-2 在此类中创建序列生成器,因为您无法使用本机查询生成序列生成器。 @Id @GeneratedValues @Column private Long seqid;

不要忘记 - 您的实体类应该扩展您的抽象类。 (帮助您的序列正常工作,它也可以在日期上工作(检查日期,我不确定))

Step- 3 在 repo 接口中编写原生查询。

value="INSERT INTO table(?,?)values(:?,:cast(:jsonString as json))",nativeQuery=true

Step - 4 这会将您的 java 字符串对象转换为 json 并在数据库中插入/存储,您还可以在每次插入时增加序列。

我在使用转换器时遇到了转换错误。我个人也避免在我的项目中使用 type-52。 如果对你们有用,请为我的 ans 投票。

【讨论】:

【参考方案10】:

我在将项目从 mysql 8.0.21 迁移到 Postgres 13 时遇到了这个问题。我的项目使用带有 Hibernate 类型依赖项版本 2.7.1 的 Spring Boot。就我而言,解决方案很简单。

我需要做的就是改变它并且它起作用了。

引用自Hibernate Types Documentation page。

【讨论】:

以上是关于将 PostgreSQL JSON 列映射到 Hibernate 实体属性的主要内容,如果未能解决你的问题,请参考以下文章

将 MySQL json 列映射到 JPA 会引发错误

将 MySQL JSON 列映射到休眠值类型

如何使用 JPA 和 Hibernate 将 MySQL JSON 列映射到 Java 实体属性

jOOQ & PostgreSQL:将从复杂 jsonb 中提取的嵌套 json 对象映射到自定义类型

如何将新项目附加到 PostgreSQL 中的数组类型列中

将 MySql 数据库中的 JSON 列映射到 Web Api 中的 C# 类