使用Hibernate 6的PostgreSQL JSON函数

这是对前一篇文章的延续,在那篇文章中描述了如何为Postgres JSON函数添加支持并使用Hibernate 5。本文将专注于如何在采用Hibernate框架版本6的项目中使用JSON操作。

原生支持

Hibernate 6已经对通过JSON属性进行查询提供了一定程度的支持,如下例所示:

我们有一个普通的实体类,它包含一个JSON属性:

Java

 

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如下所示:

Java

 

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属性进行查询。

Java

 

    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类型不需要具有复杂类型的属性时,原生支持就足够了。

请查看以下链接以获取更多信息:

然而,有时值得拥有通过数组属性进行查询的可能性。当然,我们可以在Hibernate中使用原生SQL查询,并使用上一篇文章中介绍的Postgres JSON函数。但在HQL查询中或在使用编程式断言时拥有这种可能性也很有用。第二种方法在你需要实现动态查询功能时尤其有用。虽然动态拼接一个HQL查询字符串可能很简单,但更好的做法是使用已实现的断言。这时,使用posjsonhelper库就变得非常方便。

Posjsonhelper

该项目已存在于Maven中央仓库,因此您可以轻松地通过将其作为依赖项添加到您的Maven项目中来使用它。

XML

 

<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类型。

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

另一种替代方案是在应用程序启动时使用com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher组件,如下例所示,该例使用了Spring Framework

Java

 

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“。

示例模型

我们的模型如下例所示:

Java

 

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类型。

Java

 

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操作:

SQL

 

create table item (
        id bigint not null,
        jsonb_content jsonb,
        primary key (id)
    )

为便于说明,假设我们的数据库包含以下记录:  

SQL

 


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组件和标准构建器创建:

Java

 

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代码:

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函数来包裹这些操作符。对于默认设置,这些函数的实现如下所示。

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属性包含数组且所有字符串元素均用于搜索的记录。

Java

 

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:

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属性包含数组且至少有一个字符串元素用于搜索的记录。

Java

 

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:

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_pathjsonb_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