这是对前一篇文章的延续,在那篇文章中描述了如何为Postgres JSON函数添加支持并使用Hibernate 5。本文将专注于如何在采用Hibernate框架版本6的项目中使用JSON操作。
原生支持
Hibernate 6已经对通过JSON属性进行查询提供了一定程度的支持,如下例所示:
我们有一个普通的实体类,它包含一个JSON属性:
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.Type;
import org.hibernate.type.SqlTypes;
import java.io.Serializable;
@Entity
@Table(name = "item")
public class Item implements Serializable {
@Id
private Long id;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "jsonb_content")
private JsonbContent jsonbContent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public JsonbContent getJsonbContent() {
return jsonbContent;
}
public void setJsonbContent(JsonbContent jsonbContent) {
this.jsonbContent = jsonbContent;
}
}
类型JsonbContent
如下所示:
import jakarta.persistence.Embeddable;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;
import org.hibernate.annotations.Struct;
import java.io.Serializable;
import java.util.List;
@Embeddable
public class JsonbContent implements Serializable{
private Integer integer_value;
private Double double_value;
@Enumerated(EnumType.STRING)
private UserTypeEnum enum_value;
private String string_value;
//获取器和设置器
}
当我们拥有这样的模型时,可以例如通过string_value
属性进行查询。
public List<Item> findAllByStringValueAndLikeOperatorWithHQLQuery(String expression) {
TypedQuery<Item> query = entityManager.createQuery("from Item as item_ where item_.jsonbContent.string_value like :expr", Item.class);
query.setParameter("expr", expression);
return query.getResultList();
}
重要提示!-目前,对于通过属性进行查询的支持似乎存在一些限制,即不能查询复杂类型如数组。如您所见,JsonbContent
类型带有Embeddable
注解,这意味着如果您尝试添加一个列表属性,可能会遇到以下错误信息:作为JSON序列化的预期类型不能具有复杂类型的属性:聚合组件目前可能只包含简单的基本值和简单基本值的组件。
当我们的JSON类型不需要具有复杂类型的属性时,原生支持就足够了。
请查看以下链接以获取更多信息:
- Stack Overflow: Hibernate 6.2 与 JSON 导航
- Hibernate ORM 6.2 – 复合聚合映射
- GitHub: hibernate6-tests-native-support-1
然而,有时值得拥有通过数组属性进行查询的可能性。当然,我们可以在Hibernate中使用原生SQL查询,并使用上一篇文章中介绍的Postgres JSON函数。但在HQL查询中或在使用编程式断言时拥有这种可能性也很有用。第二种方法在你需要实现动态查询功能时尤其有用。虽然动态拼接一个HQL查询字符串可能很简单,但更好的做法是使用已实现的断言。这时,使用posjsonhelper库就变得非常方便。
Posjsonhelper
该项目已存在于Maven中央仓库,因此您可以轻松地通过将其作为依赖项添加到您的Maven项目中来使用它。
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate6</artifactId>
<version>0.2.1</version>
</dependency>
注册FunctionContributor
要使用该库,我们需要附加FunctionContributor
组件。这可以通过两种方式实现。首先,最推荐的方法是在resources/META-INF/services目录下创建一个名为org.hibernate.boot.model.FunctionContributor的文件。
文件内容只需放置posjsonhelper
实现,即org.hibernate.boot.model.FunctionContributor
类型。
com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor
另一种替代方案是在应用程序启动时使用com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher
组件,如下例所示,该例使用了Spring Framework。
import com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher;
import jakarta.persistence.EntityManager;
import org.hibernate.query.sqm.NodeBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
@Configuration
public class FunctionDescriptorConfiguration implements
ApplicationListener<ContextRefreshedEvent> {
@Autowired
private EntityManager entityManager;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
NodeBuilder nodeBuilder = (NodeBuilder) entityManager.getCriteriaBuilder();
SqmFunctionRegistryEnricher sqmFunctionRegistryEnricher = new SqmFunctionRegistryEnricher();
sqmFunctionRegistryEnricher.enrich(nodeBuilder.getQueryEngine().getSqmFunctionRegistry());
}
}
更多详情请参阅”如何附加FunctionContributor“。
示例模型
我们的模型如下例所示:
package com.github.starnowski.posjsonhelper.hibernate6.demo.model;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.Type;
import org.hibernate.type.SqlTypes;
@Entity
@Table(name = "item")
public class Item {
@Id
private Long id;
@JdbcTypeCode(SqlTypes.JSON)
@Type(JsonType.class)
@Column(name = "jsonb_content", columnDefinition = "jsonb")
private JsonbContent jsonbContent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public JsonbContent getJsonbContent() {
return jsonbContent;
}
public void setJsonbContent(JsonbContent jsonbContent) {
this.jsonbContent = jsonbContent;
}
}
重要!:在此示例中,JsonbContent
属性是一个自定义类型(如下所示),但它也可以是String类型。
package com.github.starnowski.posjsonhelper.hibernate6.demo.model;
import jakarta.persistence.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.io.Serializable;
import java.util.List;
public class JsonbContent implements Serializable{
private List top_element_with_set_of_values;
private Integer integer_value;
private Double double_value;
@Enumerated(EnumType.STRING)
private UserTypeEnum enum_value;
private String string_value;
private Child child;
// 设置器和获取器
}
针对表的DDL操作:
create table item (
id bigint not null,
jsonb_content jsonb,
primary key (id)
)
为便于说明,假设我们的数据库包含以下记录:
INSERT INTO item (id, jsonb_content) VALUES (1, '{"top_element_with_set_of_values":["TAG1","TAG2","TAG11","TAG12","TAG21","TAG22"]}');
INSERT INTO item (id, jsonb_content) VALUES (2, '{"top_element_with_set_of_values":["TAG3"]}');
INSERT INTO item (id, jsonb_content) VALUES (3, '{"top_element_with_set_of_values":["TAG1","TAG3"]}');
INSERT INTO item (id, jsonb_content) VALUES (4, '{"top_element_with_set_of_values":["TAG22","TAG21"]}');
INSERT INTO item (id, jsonb_content) VALUES (5, '{"top_element_with_set_of_values":["TAG31","TAG32"]}');
-- 没有任何属性的项,仅一个空JSON
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');
-- 整数值
INSERT INTO item (id, jsonb_content) VALUES (7, '{"integer_value": 132}');
INSERT INTO item (id, jsonb_content) VALUES (8, '{"integer_value": 562}');
INSERT INTO item (id, jsonb_content) VALUES (9, '{"integer_value": 1322}');
-- 双精度值
INSERT INTO item (id, jsonb_content) VALUES (10, '{"double_value": 353.01}');
INSERT INTO item (id, jsonb_content) VALUES (11, '{"double_value": -1137.98}');
INSERT INTO item (id, jsonb_content) VALUES (12, '{"double_value": 20490.04}');
-- 枚举值
INSERT INTO item (id, jsonb_content) VALUES (13, '{"enum_value": "SUPER"}');
INSERT INTO item (id, jsonb_content) VALUES (14, '{"enum_value": "USER"}');
INSERT INTO item (id, jsonb_content) VALUES (15, '{"enum_value": "ANONYMOUS"}');
-- 字符串值
INSERT INTO item (id, jsonb_content) VALUES (16, '{"string_value": "this is full sentence"}');
INSERT INTO item (id, jsonb_content) VALUES (17, '{"string_value": "this is part of sentence"}');
INSERT INTO item (id, jsonb_content) VALUES (18, '{"string_value": "the end of records"}');
-- 内部元素
INSERT INTO item (id, jsonb_content) VALUES (19, '{"child": {"pets" : ["dog"]}}');
INSERT INTO item (id, jsonb_content) VALUES (20, '{"child": {"pets" : ["cat"]}}');
INSERT INTO item (id, jsonb_content) VALUES (21, '{"child": {"pets" : ["dog", "cat"]}}');
INSERT INTO item (id, jsonb_content) VALUES (22, '{"child": {"pets" : ["hamster"]}}');
使用标准组件
以下是与开头展示的相同查询的示例,但使用SQM组件和标准构建器创建:
public List<Item> findAllByStringValueAndLikeOperator(String expression) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> root = query.from(Item.class);
query.select(root);
query.where(cb.like(new JsonBExtractPathText(root.get("jsonbContent"), singletonList("string_value"), (NodeBuilder) cb), expression));
return entityManager.createQuery(query).getResultList();
}
Hibernate将生成如下SQL代码:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''
`jsonb_extract_path_text`是Postgres的一个函数,相当于操作符#>>
(详情请查阅之前链接的Postgres文档)。
数组操作
该库支持几种Postgres JSON函数操作符,例如:
?&
– 检查文本数组中的所有字符串是否作为顶级键或数组元素存在。因此,如果我们有一个包含数组的JSON属性,则可以检查它是否包含您正在搜索的所有元素。?|
– 检查文本数组中的任何字符串是否作为顶级键或数组元素存在。因此,如果我们有一个包含数组的JSON属性,则可以检查它是否至少包含您正在搜索的元素之一。
除了执行原生SQL查询外,Hibernate 6不支持上述操作。
必需的DDL变更
由于特殊字符的存在,上述操作符在HQL中无法直接使用,因此我们需要将其包裹,例如,通过自定义SQL函数实现。Posjsonhelper
库要求定义两个自定义SQL函数来包裹这些操作符。对于默认设置,这些函数的实现如下所示。
CREATE OR REPLACE FUNCTION jsonb_all_array_strings_exist(jsonb, text[]) RETURNS boolean AS $$
SELECT $1 ?& $2;
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION jsonb_any_array_strings_exist(jsonb, text[]) RETURNS boolean AS $$
SELECT $1 ?| $2;
$$ LANGUAGE SQL;
如需了解更多关于如何自定义或通过编程添加必需的DDL变更的信息,请查阅章节”应用DDL变更“。
“?&”包裹器
以下代码示例展示了如何创建一个查询,该查询针对的是JSON属性包含数组且所有字符串元素均用于搜索的记录。
public List<Item> findAllByAllMatchingTags(Set<String> tags) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> root = query.from(Item.class);
query.select(root);
query.where(new JsonbAllArrayStringsExistPredicate(hibernateContext, (NodeBuilder) cb, new JsonBExtractPath(root.get("jsonbContent"), (NodeBuilder) cb, singletonList("top_element_with_set_of_values")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
如果标签包含两个元素,那么Hibernate将生成如下SQL:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_all_array_strings_exist(jsonb_extract_path(i1_0.jsonb_content,?),array[?,?])
“?|”包裹器
下面的示例代码说明了如何构建一个查询,该查询关注的是JSON属性包含数组且至少有一个字符串元素用于搜索的记录。
public List<Item> findAllByAnyMatchingTags(HashSet<String> tags) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> root = query.from(Item.class);
query.select(root);
query.where(new JsonbAnyArrayStringsExistPredicate(hibernateContext, (NodeBuilder) cb, new JsonBExtractPath(root.get("jsonbContent"), (NodeBuilder) cb, singletonList("top_element_with_set_of_values")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
若标签包含两个元素,Hibernate将生成如下SQL:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_any_array_strings_exist(jsonb_extract_path(i1_0.jsonb_content,?),array[?,?])
如需更多数值运算符使用示例,请参阅dao对象演示和dao测试。
为何在Hibernate已支持JSON属性查询的情况下,还需使用posjsonhelper库
除上述支持数组类型的两个运算符外,该库还额外提供了两个实用运算符:jsonb_extract_path
与jsonb_extract_path_text
,它们是对#>
和#>>
操作符的封装。Hibernate支持->>
操作符。要了解这些操作符之间的差异,请查阅先前提供的Postgres文档链接。
然而,正如文章开头所述,对JSON属性的原生查询支持仅限于JSON类中具有简单类型属性的情况。更重要的是,如果属性未映射到JSON类型的属性,则无法通过该属性进行查询。如果你的JSON结构假设更加动态且具有不受任何模式定义的弹性结构,这可能会成为一个问题。
但使用posjsonhelper
操作符,这一问题不复存在。你可以查询任何想要的属性,无需该属性在JSON类型中被定义为属性。此外,存储JSON列的实体属性在我们的示例中无需像JsonbContent
那样是复杂对象,它可以是Java中的简单字符串。
结论
正如前一篇文章所提到的,在某些情况下,Postgres的JSON类型和函数可以作为NoSQL数据库的良好替代品。这可以帮助我们避免向技术栈中添加NoSQL解决方案,这可能会增加更多复杂性和额外成本。
这也为我们提供了在关系型数据库中存储非结构化数据时的灵活性,以及在这些结构中进行查询的可能性。
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6