使用 Hibernate 5 的 Postgres JSON 函數

Postgres資料庫支援幾種JSON類型及其特殊操作

在某些情況下,這些操作可能是類似MongoDB或其他NoSQL資料庫的文件資料庫的良好替代方案。當然,像MongoDB這樣的資料庫可能在複製過程方面表現更佳,但這一主題不在本文討論範圍之內。

本文將專注於如何在採用Hibernate框架版本5的項目中使用JSON操作。

示例模型

我們的模型如下所示:

Java

 

@Entity
@Table(name = "item")
public class Item {

    @Id
    private Long id;

    @Column(name = "jsonb_content", columnDefinition = "jsonb")
    private String jsonbContent;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getJsonbContent() {
        return jsonbContent;
    }

    public void setJsonbContent(String jsonbContent) {
        this.jsonbContent = jsonbContent;
    }
}

重要提示:我們本可以使用特定JSON類型來定義jsonbContent屬性,但在Hibernate版本5中,從操作角度看這並不會帶來任何益處。

DDL操作:

SQL

 

create table item (
       id int8 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"]}}');

原生查詢方法

在 Hibernate 5 中,我們可以使用原生方法,直接執行 SQL 命令。

重要!:請注意,為了展示目的,以下代碼允許 SQL 注入用於LIKE操作符的表達式。當然,對於此類操作,我們應該使用參數和PreparedStatement

Java

 


private EntityManager entityManager;

public List<Item> findAllByStringValueAndLikeOperatorWithNativeQuery(String expression) {
        return entityManager.createNativeQuery("SELECT * FROM item i WHERE i.jsonb_content#>>'{string_value}' LIKE '" + expression + "'", Item.class).getResultList();
    }

在上例中,使用了#>>操作符,該操作符提取指定路徑的 JSON 子對象作為文本(請參閱Postgres 文檔了解更多詳情)。

大多數情況下,這樣的查詢(當然,使用轉義值)已足夠。但如果我們需要根據 API 傳入的參數實現某種動態查詢的創建,則使用某種條件構建器會更佳。

Posjsonhelper

Hibernate 5 默認不支持Postgres JSON函數。幸運的是,您可以自行實現或使用posjsonhelper庫,這是一個開源項目。

此專案已存於Maven中央倉庫,您只需將其作為依賴添加到您的Maven專案中即可輕鬆使用。

XML

 

        <dependency>
            <groupId>com.github.starnowski.posjsonhelper</groupId>
            <artifactId>hibernate5</artifactId>
            <version>0.1.0</version>
        </dependency>

要在專案中使用posjsonhelper庫,您需使用專案內實現的Postgres方言。例如:

com.github.starnowski.posjsonhelper.hibernate5.dialects.PostgreSQL95DialectWrapper ...

若您的專案已具備自定義方言類,亦可採用以下方式:

com.github.starnowski.posjsonhelper.hibernate5.PostgreSQLDialectEnricher;

使用條件組件

以下範例與先前使用原生查詢的範例行為相似。然而,在此例中,我們將運用條件建構器。

Java

 

	private EntityManager entityManager;

    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((CriteriaBuilderImpl) cb, singletonList("string_value"), root.get("jsonbContent")), expression));
        return entityManager.createQuery(query).getResultList();
    }

Hibernate將生成如下SQL代碼:

SQL

 

select
            item0_.id as id1_0_,
            item0_.jsonb_content as jsonb_co2_0_ 
        from
            item item0_ 
        where
            jsonb_extract_path_text(item0_.jsonb_content,?) like ?

jsonb_extract_path_text是Postgres的一個函數,等同於#>>操作符(詳情請參閱先前提供的Postgres文檔鏈接)。

陣列操作

該庫支援數種Postgres JSON函數操作符,如:

  • ?& – 檢查文本陣列中的所有字串是否作為頂層鍵或陣列元素存在。一般而言,若JSON屬性包含一個陣列,則可檢查其是否包含您所搜尋的所有元素。
  • ?| – 檢查文本陣列中的任一字串是否作為頂層鍵或陣列元素存在。一般而言,若JSON屬性包含一個陣列,則可檢查其是否至少包含您所搜尋的一個元素。

所需的DDL變更

由於特殊字符,上述操作符無法在HQL中使用。這就是為什麼我們需要將它們包裹起來,例如,在一個自定義SQL函數中。Posjsonhelper庫需要兩個自定義SQL函數,這些函數將包裹這些操作符。對於默認設置,這些函數將具有以下實現。

PLSQL

 

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

 

    
	private EntityManager entityManager;

	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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
        return entityManager.createQuery(query).getResultList();
    }

如果標簽包含兩個元素,那麼Hibernate將生成以下SQL:

SQL

 

select
            item0_.id as id1_0_,
            item0_.jsonb_content as jsonb_co2_0_ 
        from
            item item0_ 
        where
            jsonb_all_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true

“?|”包裹器

下面的代碼示例說明瞭如何創建一個查詢,該查詢查看的是那些包含我們正在搜索的至少一個字符串元素的JSON屬性(該屬性包含一個數組)的記錄。

Java

 

	private EntityManager entityManager;
    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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
        return entityManager.createQuery(query).getResultList();
    }

如果標簽包含兩個元素,那麼Hibernate將生成以下SQL:

SQL

 

select
            item0_.id as id1_0_,
            item0_.jsonb_content as jsonb_co2_0_ 
        from
            item item0_ 
        where
            jsonb_any_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true

如需更多數值運算符使用範例,請參閱演示dao 物件dao 測試

結論

在某些情況下,Postgres JSON 類型及函數可作為 NoSQL 資料庫的良好替代方案。此舉有助於避免將 NoSQL 解決方案納入我們的技術堆疊,從而減少複雜性及額外成本。

Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-5