Postgres資料庫支援幾種JSON類型及其特殊操作。
在某些情況下,這些操作可能是類似MongoDB或其他NoSQL資料庫的文件資料庫的良好替代方案。當然,像MongoDB這樣的資料庫可能在複製過程方面表現更佳,但這一主題不在本文討論範圍之內。
本文將專注於如何在採用Hibernate框架版本5的項目中使用JSON操作。
示例模型
我們的模型如下所示:
@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操作:
create table item (
id int8 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"]}}');
原生查詢方法
在 Hibernate 5 中,我們可以使用原生方法,直接執行 SQL 命令。
重要!:請注意,為了展示目的,以下代碼允許 SQL 注入用於LIKE
操作符的表達式。當然,對於此類操作,我們應該使用參數和PreparedStatement
。
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專案中即可輕鬆使用。
<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;
使用條件組件
以下範例與先前使用原生查詢的範例行為相似。然而,在此例中,我們將運用條件建構器。
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代碼:
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函數,這些函數將包裹這些操作符。對於默認設置,這些函數將具有以下實現。
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屬性(該屬性包含一個數組)的記錄。
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:
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屬性(該屬性包含一個數組)的記錄。
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:
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