Это другой статья в серии, связанных с поддержкой функций JSON Postgres в проекте, использующемфреймворк Hibernate с версией 6. Тема статьи – операции модификации записей JSON. Как и в предыдущей статье, следует отметить, что Postgres может не иметь таких всесторонних операций, как другие NoSQL-базы данных, такие как MongoDB для модификации JSON ( хотя, с помощью соответствующих конструкций функций, можно достичь того же эффекта). Тем не менее, он подходит для большинства проектов, требующих модификации JSON. Кроме того, с поддержкой транзакций (что не поддерживается в NoSQL-базе данных на таком уровне), это довольно хорошая идея использовать Postgres с данными JSON. Конечно, NoSQL-базы данных имеют и другие преимущества, которые могли бы лучше соответствовать потребностям проектов.
Вообще говоря, существует много статей о поддержке JSON Postgres. Эта статья фокусируется на интеграции этой поддержки с библиотекой 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"}');
Нative SQL Execution
Как и в других фреймворках Java, с Hibernate вы можете выполнять нативные SQL-запросы — которые хорошо документированы и есть много примеров в интернете. Поэтому в этой статье мы не будем сосредотачиваться на выполнении операций нативного SQL. Тем не менее, будут приведены примеры того, какого SQL JPA операции генерируют.因为 Hibernate – это реализация JPA, это имеет смысл показать, как API JPA может изменять JSON-данные в базе данных Postgres.
Изменение свойств JSON-объекта и не всего JSON-объекта (пути)
Установка всего JSON-payload для одной колонки легко и не требует много объяснений. Мы просто устанавливаем значение для свойства в нашем классе Entity
, который представляет собой колонку с JSON-содержимым.
Также это похоже на установку одного или нескольких JSON-свойств для одной строки базы данных. Мы просто читаем строку таблицы, десериализуем JSON-значение в POJO, представляющем JSON-объект, устанавливаем значения для определенных свойств и обновляем записи базы данных с целым payload. Однако такой подход может оказаться не практичным, когда мы хотим изменить JSON-свойства для множества строк базы данных.
Предположим, что нам необходимо выполнить пакетную обновление определённых свойств JSON. Запрос данных из базы данных и обновление каждого записи может не быть эффективным методом.
Более удобно выполнить такое обновление однимupdate
запросом, где мы устанавливаем значения для определённых свойств JSON. К счастью, Postgres предоставляет функции, модифицирующие содержимое JSON, которые можно использовать в SQL запросе обновления.
Posjsonhelper
Hibernate имеет лучшую поддержку модификации JSON в версии 7, включая большинство функций и операторов, упомянутых в этой статье. Тем не менее, в версии 6 такая поддержка не планируется. К счастью, проект Posjsonhelper добавляет такую поддержку для Hibernate в версии 6. Все примеры ниже будут использовать библиотеку Posjsonhelper.Пожалуйста,тест этогоссылки, чтобы узнать, как прикрепить библиотеку к вашему Java-проекту. Вы также должны прикрепить FunctionContributor.
Во всех примерах используется класс Java entity, представляющий таблицу item
, определение которой было приведено выше:
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 Function Wrapper
Функция jsonb_set
, вероятно, является наиболее полезной функцией, когда требуется модификация JSON-данных. Она позволяет устанавливать определенные свойства для объектов JSON и конкретных элементов массива на основе индекса массива.
Например, приведенный ниже код добавляет свойство "birthday"
к внутреннему свойству "child"
.
// 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());
Код объединения JSON-объекта со свойством 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 имеет обёртку для операции удаления (#-
). Она удаляет поле или элемент массива на основании индекса указанного пути, где элементы пути могут быть либо ключами полей JSON, либо индексами массива. Например, следующий код удаляет из свойства JSON-объекта на основании JSON-пути "child.pets"
.
// 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());
Generated SQL would be:
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-массива. Этот код удаляет элементы "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\"]}");
Here is the SQL generated by the above code:
update
item
set
jsonb_content=jsonb_set(jsonb_content, ?::text[], remove_values_from_json_array(jsonb_extract_path(jsonb_content, ?), ?::jsonb))
where
id=?
Hibernate6JsonUpdateStatementBuilder: Как объединить несколько операций модификации в одно SQL-запрос на обновление.
Все вышеуказанные примеры демонстрируют выполнение одной операции, которая изменяет данные 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 function
и две операции 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, особенно с решениями, предоставляемыми cloud providers, такими как AWS), то может быть sensble рассмотреть возможность хранения ваших JSON-документов в базе данных Postgres. Не говоря уже о поддержке транзакций с базами данных, такими как Postgres.
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate