likes
comments
collection
share

如何在Hibernate 6的项目中使用Postgres JSON函数

作者站长头像
站长
· 阅读数 4

这是之前一篇文章的延续:如何在Hibernate 5框架的项目中使用Postgres JSON函数。在本文中,我们将重点介绍如何在使用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;
  //Getters and Setters
}

当我们拥有这样的模型时,我们可以通过 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 类型不需要具有复杂类型的属性时,原生支持已经足够。然而,有时查询数组属性也是有价值的。当然,我们可以在 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 组件。有两种方法可以实现这一点。第一种方法也是最推荐的方法是在 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());
    }
}

示例模型

我们的模型如下所示:

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<String> 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;

//  Setters and Getters
}

表的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"]}');

-- item without any properties, just an empty json
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

-- int values
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}');

-- double values
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}');

-- enum values
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"}');

-- string values
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"}');

-- inner elements
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"]}}');

使用Criteria组件

以下是与之前开始时提到的相同查询相对应的示例,但是是使用Sqm组件和criteria builder创建的:

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

sonb_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();
    }

如果tags包含两个元素,那么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[?,?])

"?|"

下面的示例代码演示了如何创建一个查询,该查询查找包含一个数组且至少有一个字符串元素与我们要搜索的元素匹配的记录。

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解决方案添加到技术栈中,从而增加了更多的复杂性和额外的成本。这也为我们提供了在关系数据库中存储非结构化数据并对其进行查询的灵活性。

作者:Szymon Tarnowski

更多技术干货请关注公众号“云原生数据库

squids.cn,目前可体验全网zui低价RDS,免费的迁移工具DBMotion、SQL开发工具等