如何在Hibernate中使用PostgreSQL的JSONB数据类型

763 阅读7分钟

大多数数据库除了已知的SQL标准外,还提供了很多专有功能。一个例子是PostgreSQL的JSONB数据类型,它允许你在数据库列中有效地存储JSON文档。

当然,你也可以把JSON文档存储在一个文本列中。这是SQL标准的一部分,并且被Hibernate和所有其他JPA实现所支持。尽管如此,你还是会错过PostgreSQL特有的功能,比如JSON验证和一系列有趣的JSON函数和操作符。但是,如果你正在阅读这篇文章,你可能已经知道了这些。

如果你想在Hibernate 6中使用JSONB列,我有个好消息给你。Hibernate 6为实体属性到JSON列提供了标准的映射,你只需要激活它。不幸的是,Hibernate 4和5不支持任何JSON映射,你必须要实现一个UserType。我将在这篇文章中向你展示两种选择。

数据库表和实体

在我们讨论UserType的细节之前,让我们快速浏览一下数据库表和实体。
正如你在下面的代码片段中看到的,数据库表的定义非常简单,只包括2个列:主键列idJSONB类型的列jsonproperty:

CREATE TABLE myentity
(
  id bigint NOT NULL,
  jsonproperty jsonb,
  CONSTRAINT myentity_pkey PRIMARY KEY (id)
)

而你可以在下面的代码片断中看到映射该表的实体:

@Entity
public class MyEntity {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private MyJson jsonProperty;
     
    ...
}

正如你所看到的,在这个实体上没有任何特定的JSON,只有一个MyJson类型的属性。MyJson是一个简单的POJO,有两个属性,你可以在下一个代码片段中看到:

public class MyJson implements Serializable {
 
    private String stringProp;
     
    private Long longProp;
 
    public String getStringProp() {
        return stringProp;
    }
 
    public void setStringProp(String stringProp) {
        this.stringProp = stringProp;
    }
 
    public Long getLongProp() {
        return longProp;
    }
 
    public void setLongProp(Long longProp) {
        this.longProp = longProp;
    }
}

那么,如果你想把MyJson属性存储在JSONB数据库列中,你要做什么?这个问题的答案取决于你使用的Hibernate版本。

在Hibernate 4和5中,你需要实现一个自定义类型映射。不要担心。这并不像它听起来那么复杂。你只需要实现UserType 接口并注册你的类型映射。我将在本文中告诉你如何做到这一点。

Hibernate 6让这一切变得更加简单。它提供了一个标准的JSON映射,你需要激活它。让我们先来看看这个。

Hibernate 6中的JSONB映射

由于Hibernate 6中引入了JSON映射,你只需要用*@JdbcTypeCode注解来注释你的实体属性,并将类型设置为SqlTypes.JSON*。然后Hibernate会在你的classpath上检测一个JSON库,并使用它来序列化和反序列化属性的值:

@Entity
public class MyEntity {
 
    @Id
    @GeneratedValue
    private Long id;
 
    @JdbcTypeCode(SqlTypes.JSON)
    private MyJson jsonProperty;
     
    ...
}

@JdbcTypeCode注解是一个新的注解,是作为Hibernate新类型映射的一部分而引入的。从Hibernate 6开始,你可以通过给实体属性加上*@JdbcTypeCode@JavaType注解来分别定义Java和JDBC映射。使用这些注解,你可以引用Hibernate的一个标准映射或你自己对JavaTypeDescriptor* 或JdbcTypeDescriptor 接口的实现。我将在另一篇教程中解释这些接口的实现。我们只需要激活Hibernate的标准映射。

在你注释你的实体属性以激活Hibernate的JSON映射后,你可以在你的业务代码中使用实体和它的属性。我在本文的最后准备了一个例子。

Hibernate 4和5中的JSONB映射

正如我前面提到的,如果你想在Hibernate 4或5中使用PostgreSQL的JSONB类型,你需要实现一个自定义映射。最好的方法是实现Hibernate的UserType接口并在自定义方言中注册映射。

实现一个HibernateUserType

你首先要创建一个HibernateUserType,将MyJson对象映射成JSON文档,并定义与SQL类型的映射关系。我把UserType称为MyJsonType,在下面的代码片段中只展示了最重要的方法。你可以在GitHub资源库中查看整个类。

如果你想实现你自己的UserType,有几件重要的事情你必须做。首先,你必须实现sqlTypesreturnedClass方法,这两个方法告诉Hibernate它将使用的SQL类型和Java类来实现这个映射。在本例中,我使用通用的Type.JAVA_OBJECT作为SQL类型,当然也使用MyJson类作为Java类:

public class MyJsonType implements UserType {
 
    @Override
    public int[] sqlTypes() {
        return new int[]{Types.JAVA_OBJECT};
    }
 
    @Override
    public Class<MyJson> returnedClass() {
        return MyJson.class;
    }
     
    ...
}

然后你必须实现nullSafeGetnullSafeSet方法,Hibernate将在你读取或写入属性时调用这些方法*。*

nullSafeGet方法被调用来将数据库中的值映射到Java类中。所以我们必须把JSON文档解析成MyJson类。我在这里使用JacksonObjectMapper,但你也可以使用任何其他的JSON解析器。

nullSafeSet方法实现了将MyJson类映射到JSON文档中。使用Jackson库,你可以使用与nullSafeGet 方法中相同的ObjectMapper来实现:

@Override
public Object nullSafeGet(final ResultSet rs, final String[] names, final SessionImplementor session,
                          final Object owner) throws HibernateException, SQLException {
    final String cellContent = rs.getString(names[0]);
    if (cellContent == null) {
        return null;
    }
    try {
        final ObjectMapper mapper = new ObjectMapper();
        return mapper.readValue(cellContent.getBytes("UTF-8"), returnedClass());
    } catch (final Exception ex) {
        throw new RuntimeException("Failed to convert String to Invoice: " + ex.getMessage(), ex);
    }
}
 
@Override
public void nullSafeSet(final PreparedStatement ps, final Object value, final int idx,
                        final SessionImplementor session) throws HibernateException, SQLException {
    if (value == null) {
        ps.setNull(idx, Types.OTHER);
        return;
    }
    try {
        final ObjectMapper mapper = new ObjectMapper();
        final StringWriter w = new StringWriter();
        mapper.writeValue(w, value);
        w.flush();
        ps.setObject(idx, w.toString(), Types.OTHER);
    } catch (final Exception ex) {
        throw new RuntimeException("Failed to convert Invoice to String: " + ex.getMessage(), ex);
    }
}

你需要实现的另一个重要方法是deepCopy方法,它必须创建一个MyJson对象的深度拷贝。最简单的方法之一是对MyJson对象进行序列化和反序列化。这将迫使JVM创建该对象的深度拷贝:

@Override
public Object deepCopy(final Object value) throws HibernateException {
    try {
        // use serialization to create a deep copy
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(value);
        oos.flush();
        oos.close();
        bos.close();
         
        ByteArrayInputStream bais = new ByteArrayInputStream(bos.toByteArray());
        Object obj = new ObjectInputStream(bais).readObject();
        bais.close();
        return obj;
    } catch (ClassNotFoundException | IOException ex) {
        throw new HibernateException(ex);
    }
}

注册UserType

在下一步,你需要注册你的自定义UserType。你可以在package-info.java文件中用@TypeDef注解来做这件事。正如你在下面的代码片段中看到的,我设置了*@TypeDef注解的名称typeClass*属性:

@org.hibernate.annotations.TypeDef(name = "MyJsonType", typeClass = MyJsonType.class)
 
package org.thoughts.on.java.model;

这将UserType MyJsonType 链接到 "MyJsonType"这个名字,然后我可以在实体映射中使用@Type注解:

@Entity
public class MyEntity {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", updatable = false, nullable = false)
    private Long id;
 
    @Column
    @Type(type = "MyJsonType")
    private MyJson jsonProperty;
     
    ...
 
}

我们就快完成了。Hibernate现在将使用UserType MyJsonType来保存数据库中的jsonproperty属性。但是还剩下一步。

Hibernate方言

Hibernate的PostgreSQL方言并不支持JSONB数据类型,你需要注册它。你可以通过扩展一个现有的方言并在构造函数中调用registerColumnType方法来实现。我在这个例子中使用了一个PostgreSQL数据库,并扩展了Hibernate的PostgreSQL94Dialect:

public class MyPostgreSQL94Dialect extends PostgreSQL94Dialect {
 
    public MyPostgreSQL94Dialect() {
        this.registerColumnType(Types.JAVA_OBJECT, "jsonb");
    }
}

现在你终于可以将MyJson对象存储在JSONB列中了。

如何用JSONB映射来使用一个实体

正如你在这篇文章中所看到的,将实体属性映射到JSONB 列所需要做的事情取决于你所使用的Hibernate版本。但对于你使用实体或其属性的业务代码来说,情况并非如此。你可以像其他实体一样使用MyEntity实体和它的MyJson属性。这也使得你在将应用程序迁移到Hibernate 6时,可以用Hibernate的标准处理方式替换你的UserType 实现。

下面的代码片段显示了一个简单的例子,它使用EntityManager.find方法从数据库中获取一个实体,然后改变MyJson对象的属性值:

MyEntity e = em.find(MyEntity.class, 10000L);
e.getJsonProperty().setStringProp("changed");
e.getJsonProperty().setLongProp(789L);

而如果你想根据JSON文档里面的一些属性值来选择一个实体,你可以使用PostgreSQL的JSON函数和操作符来进行本地查询:

MyEntity e = (MyEntity) em.createNativeQuery("SELECT * FROM myentity e WHERE e.jsonproperty->'longProp' = '456'", MyEntity.class).getSingleResult();

总结

PostgreSQL提供了不同的专有数据类型,比如我在这篇文章中使用的JSONB类型,用来在数据库中存储JSON文档。

Hibernate 6提供了一个标准的JSON映射。你只需要用*@JdbcTypeCode注解来激活你的实体属性,并将类型设置为SqlTypes.JSON*。

Hibernate 4和5不支持这些数据类型。你必须自己实现映射。正如你在这篇文章中看到的,你可以通过实现UserType 接口,用*@TypeDef*注解注册它,并创建一个注册列类型的Hibernate方言来实现。