使用 Hibernate 6 的 Postgres 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;
	//Getters和Setters
}

當我們擁有這樣的模型時,我們可以例如通過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;
    }
}

重要!:在此範例中,JsonbConent屬性是一個自定義類型(如下所示),但它也可以是字串類型。

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