Este é outro artigo na série relacionada à Suporte às funções JSON do Postgres em um projeto que usa o framework Hibernate com a versão 6. O tópico do artigo são as operações de modificação em registros JSON. Como no artigo anterior, vale mencionar que o Postgres pode não ter operações tão abrangentes quanto outros bancos de dados NoSQL, como o 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 se adapta a maioria dos projetos que exigem modificação de JSON. Além disso, com suporte a transações (não existente em um nível de NoSQL), é uma boa ideia usar o Postgres com dados JSON. Claro, os bancos de dados NoSQL têm outras vantagens que podem ser melhores para alguns projetos.
Existem, em geral, muitos artigos sobre o suporte ao JSON do Postgres. Este artigo se concentra na integração deste suporte com a biblioteca Hibernate 6.
Caso alguém esteja interessado em consultar dados JSON ou em pesquisas de texto usando o Postgres e o Hibernate, veja as links abaixo:
Dados de Teste
Para o artigo, vamos supor 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 outros frameworks Java, com Hibernate, você pode executar consultas SQL nativas – o que é bem documentado e há muitos exemplos na internet. É por isso que 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 de Objeto JSON sem Alterar todo o Objeto JSON (Caminho)
Ajustar todo o payload JSON para uma coluna é fácil e não requer muita explicação. Nós apenas definimos o valor para a propriedade em nossa classe Entity
, que representa uma coluna com conteúdo JSON.
É semelhante a ajustar propriedades JSON para uma linha de banco de dados. Nós apenas ler a linha da tabela, deserializar o valor JSON the 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 queremos modificar propriedades de JSON para várias linhas de banco de dados.
Suponha que precisamos fazer atualizações em lote de propriedades JSON específicas. Recuperar do banco de dados e atualizar cada registro pode não ser um método eficiente.
Seria muito melhor fazer essa atualização com uma única instrução update
na qual definimos valores para propriedades JSON específicas. 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 oferece 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 essa funcionalidade para o Hibernate na versão 6. Todos os exemplos abaixo usarão a biblioteca Posjsonhelper. Veja esselinkpara 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=?
Excluir o Campo ou Elemento do Array Baseado no Índice na Rota Específica “#-“
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 de chaves ou índices de array. Por exemplo, o código abaixo remove da propriedade do objeto JSON com base em "child.pets"
caminho 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());
O SQL gerado seria:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
Excluir Múltiplos Elementos de Array na Rota Específica
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 sua base 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. Este código remove 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 com Uma Instrução de Atualização Unica
Todos os exemplos acima demonstraram a execução de uma operação única que modifica dados JSON. Claro, nossos códigos podem ter declarações de atualização que usam muitos dos wrappers mencionados neste artigo juntos. No entanto, estar ciente de como essas operações e funções serão executadas é crucial porque 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 ilustrar isso melhor, veja 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=?
Esse código assume 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 um parâmetro.
Embora isso seja a abordagem correta, e o wrapper existente permita a criação de tais declarações de UPDATE
, pode não ser legível a partir do ponto de vista do 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 múltiplas 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\"}}");
A declaração SQL mencionada anteriormente foi gerada por esse código.
Para saber mais sobre como o construtor funciona, 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. Isto nos leva a considerar o Postgres uma boa escolha para soluções de armazenamento de documentos. Portanto, se nossa solução não requer altas performances de leitura, melhores escalas ou sharding (apesar de todas essas coisas serem alcançadas com o banco de dados Postgres, especialmente com as soluções fornecidas por provedores de nuvem como AWS), então é interessante 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