Este é outro artigo da série relacionada à suporte de funções JSON do Postgres em um projeto usando o framework Hibernate com a versão 6. O tópico do artigo é as operações de modificação em registros JSON. Como no artigo anterior, é digno de menção que o Postgres pode não ter operações tão abrangentes quanto outros bancos de dados não-relacionais, como MongoDB, para modificação de JSON (embora, com a construção apropriada de funções, seja possível alcançar o mesmo efeito). Ainda assim, ele ajusta a maioria dos projetos que exigem modificação de JSON. Além disso, com suporte a transações (não oferecido em um banco de dados não-relacional a esse nível), é uma boa ideia usar o Postgres com dados JSON. Claro, os bancos de dados não-relacionais têm outros benefícios que podem melhor ajustar aos projetos.
Existem, em geral, muitos artigos sobre o suporte do Postgres para JSON . Este artigo se concentra na integração deste suporte com a biblioteca Hibernate 6.
Caso alguém se interesse em consultar dados JSON ou busca de texto usando Postgres e Hibernate, veja as links abaixo:
Dados de Teste
Para o artigo, vamos assumir que nossa base de dados tem uma tabela chamada item
, que tem uma coluna com conteúdo JSON, como no exemplo abaixo:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
Também podemos ter alguns dados de teste:
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"}');
Execução SQL Nativa
Como em outras frameworks Java, com Hibernate, você pode executar consultas SQL nativas – o que é bem documentado e há muitos exemplos na internet. Por isso, neste artigo, não iremos focar na execução de operações de SQL nativa. No entanto, haverá exemplos do tipo de SQL que as operações JPA geram. Como Hibernate é uma implementação de JPA, faz sentido mostrar como a API JPA pode modificar dados JSON na base de dados Postgres.
Modificar Propriedades do Objeto JSON e Não o Objeto JSON inteiro (Caminho)
Ajustar todo o payload JSON para uma coluna é fácil e não requer muita explicação. Nós apenas definimos o valor das propriedades em nossa classe Entity
, que representa uma coluna com conteúdo JSON.
É semelhante a ajustar propriedades JSON únicas ou múltiplas para uma linha de banco de dados. Nós apenas ler a linha da tabela, desserializar o valor JSON para um POJO representando um objeto JSON, ajustar valores para propriedades particulares e atualizar registros de banco de dados com o payload inteiro. No entanto, essa abordagem pode não ser prática quando você quiser modificar propriedades JSON de várias linhas de banco de dados.
Suponha que precisamos fazer atualizações em lote de propriedades de JSON específicas. Recuperar do banco de dados e atualizar cada registro pode não ser uma forma eficiente.
Seria muito melhor fazer essa atualização com uma única instrução update
na qual definimos valores para propriedades de JSON particulares. Felizmente, o Postgres tem funções que modificam o conteúdo JSON e podem ser usadas na instrução SQL de atualização.
Posjsonhelper
O Hibernate tem melhor suporte para modificação de JSON na versão 7, incluindo a maioria das funções e operadores mencionados neste artigo. Ainda assim, não há planos para adicionar esse suporte na versão 6. Felizmente, o projeto Posjsonhelper adiciona essesuportepara o Hibernate na versão 6. Todos os exemplos abaixo usarão a biblioteca Posjsonhelper.Veja estelinkpara descobrir como anexar uma biblioteca the seu projeto Java. Você também terá que anexar FunctionContributor.
Todos os exemplos utilizam a classe de entidade Java que representa a tabela item
, cuja definição foi mencionada acima:
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
A função jsonb_set
é provavelmente a função mais útil quando é necessário modificar dados JSON. Ela permite que propriedades específicas para objetos JSON e elementos específicos do array sejam definidos com base no índice do array.
Por exemplo, o código abaixo adiciona a propriedade "birthday"
à propriedade interna "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());
Este código geraria uma instrução SQL desse tipo:
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=?
Envoltório do operador de concatenação “||”
O envoltório para o operador de concatenação (||
) concatena dois valores JSONB em um novo valor JSONB.
Com base na documentação do Postgres, o comportamento do operador é o seguinte:
Concatenar dois arrays gera um array contendo todos os elementos de cada entrada. A concatenação de dois objectos gera um objeto que contém a união das suas chaves, tomando o valor do segundo objeto quando existem chaves duplicadas. Todos os outros casos são tratados convertendo uma entrada não array em um array de elemento único, e então procedendo como para dois arrays. Não opera recursivamente: apenas o array de nível superior ou a estrutura do objeto é mesclada.
Aqui está um exemplo de como usar esse wrapper no seu código:
// 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());
Codifique a mesclagem de um objeto JSON com a propriedade child
com o objeto JSON já armazenado no banco de dados.
Este código gera uma consulta 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=?
Exclua o Campo ou Elemento do Array Baseado no Índice na Rota Especificada “#-“
O Posjsonhelper tem uma wrapper para a operação de exclusão (#-
). Ela exclui o campo ou elemento do array com base no índice na rota especificada, onde os elementos da rota podem ser campos chaves ou índices de array. Por exemplo, o código abaixo remove da propriedade do objeto JSON com base na rota "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());
O SQL gerado seria:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
Excluir Múltiplos Elementos de Array na Rota Especificada
Por padrão, o Postgres (pelo menos na versão 16) não tem uma função interna que permite a remoção de elementos de array com base em seu valor. No entanto, ele tem o operador interno, -#
, que mencionamos acima, que ajuda a excluir elementos de array com base no índice mas não em seu valor.
Para esse fim, o Posjsonhelper pode gerar uma função que deve ser adicionada à operação DDL e executada em seu banco de dados.
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;
Um dos wrappers usará essa função para permitir a exclusão de múltiplos valores do array JSON. Esse código remove os elementos "mask"
e "compass"
para a propriedade "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\"]}");
Aqui está o SQL gerado pelo código acima:
update
item
set
jsonb_content=jsonb_set(jsonb_content, ?::text[], remove_values_from_json_array(jsonb_extract_path(jsonb_content, ?), ?::jsonb))
where
id=?
Hibernate6JsonUpdateStatementBuilder: Como Combinar Múltiplas Operações de Modificação em Uma Instrução de Atualização Unica.
Todos os exemplos acima mostraram a execução de uma operação única que modifica dados JSON. Claro, nossos comandos de atualização no código podem usar muitos dos wrappers mencionados neste artigo juntos. No entanto, ficar ciente de como essas operações e funções serão executadas é crucial porque isso faz sentido quando o resultado da primeira operação de JSON é um input para as operações de modificação de JSON seguintes. O output dessa operação seria um input para a próxima operação, e assim por diante, até a última operação de modificação de JSON.
Para melhor ilustrar isso, verifique o código 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=?
Este pressupõe que temos quatro execuções da função jsonb_set
e duas operações delete
. A operação mais aninhada de delete
é a primeira operação de modificação de JSON porque o valor original de uma coluna que armazena dados JSON é passado como parâmetro.
Embora isso seja a abordagem correta, e o wrapper existente permita a criação de tais UPDATE
statements, pode não ser lido a partir de uma perspectiva de código. Felizmente, o Posjsonhelper tem um componente construtor que torna a construção de tais declarações complexas fácil.
O tipo Hibernate6JsonUpdateStatementBuilder
permite a construção de declarações de atualização com várias operações que modificam JSON e dependem umas das outras.
Abaixo está um exemplo de código:
// 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\"}}");
O comando SQL mencionado anteriormente foi gerado por este código.
Para saber mais sobre o funcionamento do construtor, por favor, verifique a documentação.
Conclusão
O banco de dados Postgres oferece uma ampla gama de possibilidades em relação às operações de modificação de dados em formato JSON. Isso nos leva a considerar o Postgres uma boa escolha para armazenamento de documentos. Portanto, se nossa solução não exigir performance de leitura superior, melhor escalabilidade ou sharding (embora todas essas coisas possam ser alcançadas com o banco de dados Postgres, especialmente com as soluções fornecidas por provedores de nuvem como AWS), então é bom considerar armazenar seus documentos JSON em um banco de dados Postgres — não mencionando ainda o suporte a transações com bancos de dados como o Postgres.
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate