大多数数据库除了已知的SQL标准外,还提供了很多专有功能。一个例子是PostgreSQL的JSONB数据类型,它允许你在数据库列中有效地存储JSON文档。
当然,你也可以把JSON文档存储在一个文本列中。这是SQL标准的一部分,并且被Hibernate和所有其他JPA实现所支持。尽管如此,你还是会错过PostgreSQL特有的功能,比如JSON验证和一系列有趣的JSON函数和操作符。但是,如果你正在阅读这篇文章,你可能已经知道了这些。
如果你想在Hibernate 6中使用JSONB列,我有个好消息给你。Hibernate 6为实体属性到JSON列提供了标准的映射,你只需要激活它。不幸的是,Hibernate 4和5不支持任何JSON映射,你必须要实现一个UserType。我将在这篇文章中向你展示两种选择。
数据库表和实体
在我们讨论UserType的细节之前,让我们快速浏览一下数据库表和实体。
正如你在下面的代码片段中看到的,数据库表的定义非常简单,只包括2个列:主键列id和JSONB类型的列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,有几件重要的事情你必须做。首先,你必须实现sqlTypes和returnedClass方法,这两个方法告诉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;
}
...
}
然后你必须实现nullSafeGet和nullSafeSet方法,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方言来实现。