这是关于在使用Hibernate框架(版本6.)的项目中支持Postgres JSON函数的系列文章中的另一篇文章,本文的主题是对JSON记录的修改操作。与前一篇文章一样,值得一提的是,Postgres 现在可能不像其他 NoSQL 数据库(如 MongoDB)那样拥有对 JSON 进行修改的全面操作(不过,通过适当的函数构造,也可以达到同样的效果)。它仍然适合大多数需要修改 JSON 的项目。此外,Postgres 支持事务(NoSQL 数据库不支持这种级别的事务),因此使用 Postgres 处理 JSON 数据是个不错的主意。当然,NoSQL 数据库还有其他优点,可能适合更好的项目。
关于 Postgres 对 JSON的支持,一般有很多文章。本文重点介绍如何将该支持与 Hibernate 6 库集成。
如果有人对使用 Postgres 和 Hibernate 查询 JSON 数据或进行文本搜索感兴趣,请参阅以下链接:
测试数据
假设我们的数据库有一个名为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载荷是容易的,并且不需要太多解释。我们只需在代表JSON内容的列的Entity
类中设置属性值。
对于单个数据库行,设置JSON的单个或多个属性也是类似的。我们只需读取表行,将JSON值反序列化为表示JSON对象的POJO,为特定属性设置值,并使用整个载荷更新数据库记录。然而,当想要修改多个数据库行的JSON属性时,这种方法可能并不实用。
假设我们必须对特定的JSON属性进行批量更新。从数据库中获取并更新每个记录可能不是有效的方法。
使用一个update
语句进行此类更新会更好,在该语句中我们为特定的JSON属性设置值。幸运的是,Postgres有修改JSON内容的函数,并可以在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 函数包装器
当需要修改JSON数据时,jsonb_set
函数可能是最有帮助的函数。它允许根据数组索引设置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对象合并。
此代码生成的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=?
在指定路径删除多个数组元素
默认情况下,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 数组中删除多个值。这段代码删除了 "child.inventory"
属性中的 "mask"
和 "compass"
元素。
// 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操作的结果是后续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有一个构建器组件,使构建这样的复杂语句变得容易。
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