這是與支持 Postgres JSON功能 在使用了Hibernate框架且版本為6的项目中相關的一系列文章中的另一篇文章。文章的主題是對JSON記錄進行修改操作。與前一文一樣,值得一提的是,Postgres可能如今沒有像其他NoSQL數據庫MongoDB那樣全面的JSON修改操作(儘管如此,透過適當的功能結構,還是能夠達到同樣的效果)。它仍然是大多數需要JSON修改的項目中所適合使用的。此外,具有事務支持(NoSQL數據庫在這種層面上並不受支持),使用具有JSON數據的Postgres是一個很不錯的選擇。當然,NoSQL數據庫也有其他可能更适合項目的優點。
通常有許多關於Postgres支持JSON的文章。這篇文章著重於將此支持與Hibernate 6庫整合。
假如有人对查询JSON数据或使用Postgres和Hibernate进行文本搜索感兴趣,请参阅以下链接:
測試數據
為了文章,假設我們的数据库有一個名為item
的表,其中有一個列含有JSON內容,如下面的示例所示:
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"]}');
-- 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}');
-- 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}');
-- enum values
INSERT INTO item (id, jsonb_content) VALUES (13, '{"enum_value": "SUPER"}');
-- string values
INSERT INTO item (id, jsonb_content) VALUES (18, '{"string_value": "the end of records"}');
原生SQL執行
像其他Java框架一樣,使用Hibernate,您可以运行原生SQL查询 — 这在文档中有很好的记载,互联网上也有很多例子。这就是为什么在这篇文章中,我们不会重点关注原生SQL操作执行。然而,将会有例子展示JPA操作生成的SQL类型。因为Hibernate是一个JPA实现,所以展示如何通过JPA API修改Postgres数据库中的JSON数据是有意义的。
修改JSON对象屬性而非整個JSON对象(路徑)
为一个列设置整个JSON载荷是容易的,并且不需要太多解释。我们只需在我们的Entity
类中设置代表JSON内容的列的值。
这类似于为数据库中的一行设置单个或多个JSON属性。我们只需读取表行,将JSON值反序列化为表示JSON对象的POJO,为特定属性设置值,并将整个载荷更新到数据库记录中。然而,当想要修改数据库中多行JSON属性时,这种方法可能并不实用。
假設我們必須對特定 JSON 屬性進行大批量更新。從數據庫中取出並更新每個記錄可能並不是一種有效的方法。
使用一個 update
語句進行這種更新會好得多,我們可以在其中為特定 JSON 屬性設定值。幸運的是,Postgres 有一些可以 modify JSON content 的函數,並可以用於 SQL 更新語句中。
Posjsonhelper
Hibernate 在第 7 版中对 JSON 修改的支持更好,包括本文中提到的绝大多数函數和運算符。然而,第 6 版中沒有計劃添加這樣的支持。幸運的是,Posjsonhelper 項目為 Hibernate 第 6 版增加了這樣的支持。下面的所有示例都将使用 Posjsonhelper 庫。支持。查看這個链接以了解如何將庫附加到你的 Java 項目。你還將需要 附加 FunctionContributor。
所有例子都使用代表 item
表的 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;
name = "item") (
public class Item implements Serializable {
private Long id;
SqlTypes.JSON) (
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;
}
}
jsonb_set 函數包装器
jsonb_set
函數在修改 JSON 數據時大概是最有用的函數。它允許根據數組索引設定 JSON 對象的特定屬性和特定數組元素。
例如,以下代碼向內置屬性 "child"
添加了屬性 "birthday"
。
// GIVEN
Long itemId = 19L;
String property = "birthday";
String value = "1970-01-01";
String expectedJson = "{\"child\": {\"pets\" : [\"dog\"], \"birthday\": \"1970-01-01\"}}";
// when
CriteriaUpdate<Item> criteriaUpdate = entityManager.getCriteriaBuilder().createCriteriaUpdate(Item.class);
Root<Item> root = criteriaUpdate.from(Item.class);
// Set the property you want to update and the new value
criteriaUpdate.set("jsonbContent", new JsonbSetFunction((NodeBuilder) entityManager.getCriteriaBuilder(), root.get("jsonbContent"), new JsonTextArrayBuilder().append("child").append(property).build().toString(), JSONObject.quote(value), hibernateContext));
// Add any conditions to restrict which entities will be updated
criteriaUpdate.where(entityManager.getCriteriaBuilder().equal(root.get("id"), itemId));
// Execute the update
entityManager.createQuery(criteriaUpdate).executeUpdate();
// then
Item item = tested.findById(itemId);
assertThat((String) JsonPath.read(item.getJsonbContent(), "$.child." + property)).isEqualTo(value);
JSONObject jsonObject = new JSONObject(expectedJson);
DocumentContext document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo(jsonObject.toString());
這行程式碼會產生如下的 SQL 語句:
update
item
set
jsonb_content=jsonb_set(jsonb_content, ?::text[], ?::jsonb)
where
id=?
Hibernate:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
i1_0.id=?
連接運算符 (||
) 包装器
連接運算符 (||
) 的包装器將兩個 JSONB 值連接成一个新的 JSONB 值。
根據 Postgres 文档,該運算符的行為如下:
連接兩個數組會產生包含每個輸入值的數組。連接兩個物件會產生一個含有它們鍵的并集的物件,當有重複的鍵時取第二個物件的值。對於所有其他情況,都將非數組輸入轉換為單個元素的數組,然後像處理兩個數組一樣進行操作。不會進行遞歸操作:只合併顶层數組或物件結構。
以下是一個如何在您的代碼中使用這個包装器的示例:
// GIVEN
Long itemId = 19l;
String property = "birthday";
String value = "1970-01-01";
// WHEN
CriteriaUpdate<Item> criteriaUpdate = entityManager.getCriteriaBuilder().createCriteriaUpdate(Item.class);
Root<Item> root = criteriaUpdate.from(Item.class);
JSONObject jsonObject = new JSONObject();
jsonObject.put("child", new JSONObject());
jsonObject.getJSONObject("child").put(property, value);
criteriaUpdate.set("jsonbContent", new ConcatenateJsonbOperator((NodeBuilder) entityManager.getCriteriaBuilder(), root.get("jsonbContent"), jsonObject.toString(), hibernateContext));
criteriaUpdate.where(entityManager.getCriteriaBuilder().equal(root.get("id"), itemId));
entityManager.createQuery(criteriaUpdate).executeUpdate();
// THEN
Item item = tested.findById(itemId);
assertThat((String) JsonPath.read(item.getJsonbContent(), "$.child." + property)).isEqualTo(value);
JSONObject expectedJsonObject = new JSONObject().put(property, value);
DocumentContext document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$.child"));
assertThat(document.jsonString()).isEqualTo(expectedJsonObject.toString());
代碼將具有 child
屬性的 JSON 物件與数据库中已經存储的 JSON 物件合併。
這行程式碼會產生如下的 SQL 查詢:
update
item
set
jsonb_content=jsonb_content || ?::jsonb
where
id=?
Hibernate:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
i1_0.id=?
根据指定路徑刪除字段或陣列元素 “
Posjsonhelper 有一個關於刪除操作 (#-
) 的包裝。它根據指定路徑的索引來刪除字段或陣列元素,其中路徑元素可以是字段鍵或陣列索引。例如,以下代碼根據 "child.pets"
JSON 路徑從 JSON 物件屬性中移除。
// GIVEN
Item item = tested.findById(19L);
JSONObject jsonObject = new JSONObject("{\"child\": {\"pets\" : [\"dog\"]}}");
DocumentContext document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo(jsonObject.toString());
// WHEN
CriteriaUpdate<Item> criteriaUpdate = entityManager.getCriteriaBuilder().createCriteriaUpdate(Item.class);
Root<Item> root = criteriaUpdate.from(Item.class);
// Set the property you want to update and the new value
criteriaUpdate.set("jsonbContent", new DeleteJsonbBySpecifiedPathOperator((NodeBuilder) entityManager.getCriteriaBuilder(), root.get("jsonbContent"), new JsonTextArrayBuilder().append("child").append("pets").build().toString(), hibernateContext));
// Add any conditions to restrict which entities will be updated
criteriaUpdate.where(entityManager.getCriteriaBuilder().equal(root.get("id"), 19L));
// Execute the update
entityManager.createQuery(criteriaUpdate).executeUpdate();
// THEN
entityManager.refresh(item);
jsonObject = new JSONObject("{\"child\": {}}");
document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo(jsonObject.toString());
生成的 SQL 會是:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
刪除指定路徑的多個陣列元素
default, Postgres (至少在版本 16 中) 沒有內置功能允許根據值移除陣列元素。但是,它有內置運算符 -#
,我們上面提到过,這有助於根據索引移除陣列元素,而不是它們的值。
為此目的,Posjsonhelper 能夠生成一個必須添加到 DDL 操作並 在您的數據庫上执行的函數。
CREATE OR REPLACE FUNCTION {{schema}}.remove_values_from_json_array(input_json jsonb, values_to_remove jsonb) RETURNS jsonb AS $$
DECLARE
result jsonb;
BEGIN
IF jsonb_typeof(values_to_remove) <> 'array' THEN
RAISE EXCEPTION 'values_to_remove must be a JSON array';
END IF;
result := (
SELECT jsonb_agg(element)
FROM jsonb_array_elements(input_json) AS element
WHERE NOT (element IN (SELECT jsonb_array_elements(values_to_remove)))
);
RETURN COALESCE(result, '[]'::jsonb);
END;
$$ LANGUAGE plpgsql;
其中一個包裝將使用此函數,允許從 JSON 陣列中刪除多個值。此代碼移除了 "mask"
和 "compass"
對於 "child.inventory"
屬性的元素。
// GIVEN
Item item = tested.findById(24L);
DocumentContext document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo("{\"child\":{\"pets\":[\"crab\",\"chameleon\"]},\"inventory\":[\"mask\",\"fins\",\"compass\"]}");
CriteriaUpdate<Item> criteriaUpdate = entityManager.getCriteriaBuilder().createCriteriaUpdate(Item.class);
Root<Item> root = criteriaUpdate.from(Item.class);
NodeBuilder nodeBuilder = (NodeBuilder) entityManager.getCriteriaBuilder();
JSONArray toRemoveJSONArray = new JSONArray(Arrays.asList("mask", "compass"));
RemoveJsonValuesFromJsonArrayFunction deleteOperator = new RemoveJsonValuesFromJsonArrayFunction(nodeBuilder, new JsonBExtractPath(root.get("jsonbContent"), nodeBuilder, Arrays.asList("inventory")), toRemoveJSONArray.toString(), hibernateContext);
JsonbSetFunction jsonbSetFunction = new JsonbSetFunction(nodeBuilder, (SqmTypedNode) root.get("jsonbContent"), new JsonTextArrayBuilder().append("inventory").build().toString(), deleteOperator, hibernateContext);
// Set the property you want to update and the new value
criteriaUpdate.set("jsonbContent", jsonbSetFunction);
// Add any conditions to restrict which entities will be updated
criteriaUpdate.where(entityManager.getCriteriaBuilder().equal(root.get("id"), 24L));
// WHEN
entityManager.createQuery(criteriaUpdate).executeUpdate();
// THEN
entityManager.refresh(item);
document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo("{\"child\":{\"pets\":[\"crab\",\"chameleon\"]},\"inventory\":[\"fins\"]}");
以下是由上方代碼生成的 SQL:
update
item
set
jsonb_content=jsonb_set(jsonb_content, ?::text[], remove_values_from_json_array(jsonb_extract_path(jsonb_content, ?), ?::jsonb))
where
id=?
Hibernate6JsonUpdateStatementBuilder: 如何在一個更新語句中結合多個修改操作
所有上述示例都展示了修改JSON數據的單個操作的執行。當然,我們的代碼中可以有使用本篇文章中提到的許多包装器在一起的更新語句。然而,了解這些操作和函數將如何執行是至關重要的,因為當第一個JSON操作的結果是 following JSON修改操作的輸入時,這才最有意義。這個操作的輸出將是下一個操作的輸入,依此類推,直到最後一個JSON修改操作。
為更好地说明這一點,請查看SQL代碼。
update
item
set
jsonb_content=
jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
(
(jsonb_content #- ?::text[]) -- the most nested #- operator
#- ?::text[])
, ?::text[], ?::jsonb) -- the most nested jsonb_set operation
, ?::text[], ?::jsonb)
, ?::text[], ?::jsonb)
, ?::text[], ?::jsonb)
where
id=?
這假定我們有四個jsonb_set 函數
的執行和兩個delete
操作。最嵌套的delete
操作是第一個JSON修改操作,因為存儲JSON數據的列的原始值作為參數傳遞。
雖然這是正確的方法,並且现有的包裝器允許創建這樣的UPDATE
語句,但从代碼角度看可能會不易於閱讀。幸運的是,Posjsonhelper有一個建造部件,使建造這種複雜語句變得簡單。
type Hibernate6JsonUpdateStatementBuilder
允許構建具有修改JSON的多個操作且相互依賴的更新語句。
以下是一個代碼示例:
// GIVEN
Item item = tested.findById(23L);
DocumentContext document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo("{\"child\":{\"pets\":[\"dog\"]},\"inventory\":[\"mask\",\"fins\"],\"nicknames\":{\"school\":\"bambo\",\"childhood\":\"bob\"}}");
CriteriaUpdate<Item> criteriaUpdate = entityManager.getCriteriaBuilder().createCriteriaUpdate(Item.class);
Root<Item> root = criteriaUpdate.from(Item.class);
Hibernate6JsonUpdateStatementBuilder hibernate6JsonUpdateStatementBuilder = new Hibernate6JsonUpdateStatementBuilder(root.get("jsonbContent"), (NodeBuilder) entityManager.getCriteriaBuilder(), hibernateContext);
hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("child").append("birthday").build(), quote("2021-11-23"));
hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("child").append("pets").build(), "[\"cat\"]");
hibernate6JsonUpdateStatementBuilder.appendDeleteBySpecificPath(new JsonTextArrayBuilder().append("inventory").append("0").build());
hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("parents").append(0).build(), "{\"type\":\"mom\", \"name\":\"simone\"}");
hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("parents").build(), "[]");
hibernate6JsonUpdateStatementBuilder.appendDeleteBySpecificPath(new JsonTextArrayBuilder().append("nicknames").append("childhood").build());
// Set the property you want to update and the new value
criteriaUpdate.set("jsonbContent", hibernate6JsonUpdateStatementBuilder.build());
// Add any conditions to restrict which entities will be updated
criteriaUpdate.where(entityManager.getCriteriaBuilder().equal(root.get("id"), 23L));
// WHEN
entityManager.createQuery(criteriaUpdate).executeUpdate();
// THEN
entityManager.refresh(item);
document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
assertThat(document.jsonString()).isEqualTo("{\"child\":{\"pets\":[\"cat\"],\"birthday\":\"2021-11-23\"},\"parents\":[{\"name\":\"simone\",\"type\":\"mom\"}],\"inventory\":[\"fins\"],\"nicknames\":{\"school\":\"bambo\"}}");
前面提到的SQL語句就是由這段代碼生成的。
想了解更多關於建造者的運作方式,請查看文件。
結論
Postgres資料庫在JSON數據修改操作方面具有廣泛的可能性。這讓我們考慮將Postgres視為一個不錯的文件存儲解決方案。所以,如果我們的解決方案不需要更高的讀取性能、更好的擴展性或分片(儘管這些都可以通過Postgres資料庫實現,特別是通過像AWS這樣的雲服務提供商提供的解決方案),那麼考慮將您的JSON 文档存儲在Postgres資料庫中是有價值的——更何論Postgres對像样的事務支持。
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate