繁体   English   中英

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

[英]Mapping PostgreSQL JSON column to a Hibernate entity property

我的 PostgreSQL 数据库 (9.2) 中有一个包含 JSON 类型列的表。 我好不容易把map这个栏目改成了JPA2 Entity字段类型。

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

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

@Entity
public class MyEntity {

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

    public MyEntity() {
    }
}

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

如果您有兴趣,这里有一些代码片段可以让 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。 下面的实现将字符串值映射到 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

请参阅 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 指出这个问题。

Maven 依赖

您需要做的第一件事是在您的项目pom.xml配置文件中设置以下Hibernate Types Maven 依赖项:

<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 或更高版本,那么Postgre92Dialect自动注册JSON类型。

否则,您需要自己注册:

public class PostgreSQLDialect extends PostgreSQL91Dialect {

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

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

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

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

我在执行本机查询(通过 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 回答)。

有一个更容易做到这一点,它不涉及使用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

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

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

以上所有解决方案对我都不起作用。 最后,我使用本机查询来插入数据。

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

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

步骤 3 在 repo 界面中编写本机查询。

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

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

我在使用转换器工作时遇到了铸造错误。 我个人也避免在我的项目中使用 type-52。 如果对你们有用,请给我的答案点赞。

当我将我的项目从 MySQL 8.0.21 迁移到 Postgres 13 时,我遇到了这个问题。我的项目使用 Spring Boot 和 Hibernate 类型依赖版本 2.7.1。 就我而言,解决方案很简单。 在此处输入图片说明

我需要做的就是改变它,它奏效了。

Hibernate 类型文档页面引用。

我遇到column "roles" is of type json but expression is of type character varying异常,带有 Postgres 的以下实体:

@Entity
@TypeDefs(@TypeDef(name = "json", typeClass = JsonBinaryType.class))
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode(of = "extId")
public class ManualTaskUser {

    @Id
    private String extId;

    @Type(type = "json")
    @Column(columnDefinition = "json")
    private Set<Role> roles;

}

应该提到的是Role是一个枚举而不是 POJO。

在生成的 SQL 中,我可以看到 Set 已正确序列化,如下所示: ["SYSTEM","JOURNEY","ADMIN","OBJECTION","DEVOPS","ASSESSMENT"]

TypeDef注解中的typeClassJsonStringType改为JsonBinaryType解决了问题! 感谢Joseph Waweru的提示!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM