Questo è un altro articolo della serie relativa al supporto delle funzioni JSON di Postgres in un progetto che utilizza il framework Hibernate versione 6. Il soggetto dell’articolo sono le operazioni di modifica sui record JSON. Come negli articoli precedenti, vale la pena menzionare che Postgres potrebbe non avere operazioni così complete come altri database NoSQL come MongoDB per la modifica di JSON (sebbene, con le costruzioni di funzione appropriate, sia possibile ottenere lo stesso effetto). Ancora una volta, si adatta a molti progetti che richiedono la modifica di JSON. Inoltre, supportando le transazioni (funzionalità non presente in un database NoSQL a questo livello), è una scelta piuttosto buona utilizzare Postgres con dati JSON. Certamente, i database NoSQL hanno altri benefici che potrebbero essere più adatti a determinati progetti.
Si trovano generalmente molti articoli sull’ supporto diJSON di Postgres. Questo articolo si concentra sull’integrazione di questo supporto con la libreria Hibernate 6.
In caso qualcuno sia interessato alla ricerca di dati JSON o alla ricerca testuale utilizzando Postgres e Hibernate, si prega di consultare i link sottostanti:
Dati di test
Pensando al nostro articolo, supponiamo che la nostra base dati abbia una tabella chiamata item
, che ha una colonna con contenuto JSON, come nell’esempio seguente:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
Potremmo anche avere alcuni dati di testo:
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"}');
Esecuzione SQL Naturale
Come in altri framework Java, con Hibernate è possibile eseguire query SQL native — che sono ben documentate e ci sono molti esempi sulla rete. Per questo motivo, questo articolo non si concentrerà sull’esecuzione delle operazioni di SQL native. tuttavia, ci saranno esempi di ciò che tipo di SQL le operazioni JPA generano. Poiché Hibernate è una implementazione di JPA, ha senso mostrare come l’API JPA possa modificare i dati JSON nel database Postgres.
Modifica le proprietà dell’oggetto JSON e non l’intero oggetto JSON (Percorso)
Impostare l’intero payload JSON per una colonna è facile e non richiede spiegazioni dettagliate. Semplicemente impostiamo il valore della proprietà nella nostra classe Entity
, che rappresenta una colonna con contenuto JSON.
È simile ad impostare singole o più proprietà JSON per una riga di database. Leggiamo semplicemente la riga della tabella, deserializziamo il valore JSON in un POJO che rappresenta un oggetto JSON, impostiamo i valori per proprietà specifiche e aggiorniamo i record del database con l’intero payload. Tuttavia, questo approcio potrebbe non essere pratico quando vogliamo modificare le proprietà JSON per molte righe del database.
Supponiamo di dover eseguire aggiornamenti di batch su proprietà JSON specifiche. Recuperare il database e aggiornare ogni record potrebbe non essere un metodo efficiente.
Sarebbe molto meglio eseguire un aggiornamento come questo con una sola istruzione update
in cui impostiamo i valori per proprietà JSON specifiche. Fortunatamente, Postgres offre funzioni in grado di modificare il contenuto JSON e possono essere utilizzate nell’istruzione SQL di aggiornamento.
Posjsonhelper
Hibernate offre un migliore supporto per la modifica di JSON nella versione 7, inclusi la maggior parte delle funzioni e operatori menzionati in questo articolo. Comunque, non ci sono piani per aggiungere questo supporto nella versione 6. Fortunatamente, il progetto Posjsonhelper aggiunge questo supportoper Hibernate nella versione 6. Tutti gli esempi riportati di seguito useranno la libreria Posjsonhelper. Consultare questo link per scoprire come allegare una libreria al proprio progetto Java. Dovrai anche allegare FunctionContributor.
Tutti gli esempi utilizzano la classe entità Java che rappresenta la tabella item
, la cui definizione è stata menzionata in precedenza:
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
La funzione jsonb_set
è probabilmente la funzione più utile quando è necessario modificare i dati JSON. Consente di impostare proprietà specifiche per gli oggetti JSON e specifici elementi di array, in base all’indice dell’array.
Ad esempio, il codice seguente aggiunge la proprietà "compleanno"
alla proprietà interna "bambino"
.
// 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());
Questo codice genererebbe un’istruzione SQL di questo 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=?
Concatenation Operator Wrapper “||”
Il wrapper per l’operatore concatenation (||
) concatena due valori JSONB in un nuovo valore JSONB.
Sulla base della documentazione di Postgres, il comportamento dell’operatore è il seguente:
Concatenare due array genera un array contenente tutti gli elementi di ciascun input. La concatenazione di due oggetti genera un oggetto contenente l’unione delle loro chiavi, prendendo il valore del secondo oggetto quando ci sono chiavi duplicate. Tutti gli altri casi vengono trattati convertendo un input non array in un array a singolo elemento e procedendo poi come per due array. Non opera in modo ricorsivo: viene unito solo l’array di primo livello o la struttura dell’oggetto.
Ecco un esempio di come utilizzare questo wrapper nel codice:
// 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());
Codice che unisce un oggetto JSON con la proprietà child
con l’oggetto JSON già memorizzato nel database.
Questo codice genera una query SQL di questo tipo:
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=?
Elimina il campo o l’elemento dell’array in base all’indice specificato nel percorso “#-“
Il Posjsonhelper include unwrapper per l’operazione di eliminazione (#-
). Elimina il campo o l’elemento dell’array in base all’indice specificato nel percorso, in cui i membri del percorso possono essere chiavi di campi o indici di array. Per esempio, il codice seguente rimuove dalla proprietà dell’oggetto JSON in base al percorso 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());
Il SQL generato sarebbe:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
Elimina multipli elementi dell’array presso il percorso specificato
Per default, Postgres (almeno nella versione 16) non ha una funzione integrata che consente la rimozione di elementi dell’array in base al loro valore. tuttavia, dispone dell’operatore integrato -#
, che abbiamo menzionato prima, che aiuta a eliminare elementi dell’array in base all’indice ma non al loro valore.
A questo scopo, il Posjsonhelper può generare una funzione che deve essere aggiunta all’operazione DDL e eseguita sul tuo database.
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;
Uno degli wrapper userà questa funzione per consentire l’eliminazione di molti valori dall’array JSON. questo codice rimuove gli elementi "mask"
e "compass"
per la proprietà "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\"]}");
Ecco il SQL generato dal codice precedente:
update
item
set
jsonb_content=jsonb_set(jsonb_content, ?::text[], remove_values_from_json_array(jsonb_extract_path(jsonb_content, ?), ?::jsonb))
where
id=?
Hibernate6JsonUpdateStatementBuilder: Come combinare multipli operazioni di modifica in una sola istruzione di aggiornamento.
Tutti gli esempi precedenti hanno dimostrato l’esecuzione di una singola operazione che modifica i dati JSON. Naturalmente, possiamo avere istruzioni di aggiornamento nel nostro codice che usino molti wrapper menzionati in questo articolo insieme. Tuttavia, è fondamentale essere consapevoli di come queste operazioni e funzioni verranno eseguite perché è più logico quando il risultato dell’ prima operazione JSON è l’input per le operazioni di modifica JSON successive. L’output di quell’operazione sarebbe l’input per l’operazione successiva, e così via, fino all’ultima operazione di modifica JSON.
Per illustrare meglio questo concetto, controllate il codice 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=?
Questo presuppone che abbiamo quattro esecuzioni della funzione jsonb_set
e due operazioni delete
. L’operazione delete
più ricca di nodi è una prima operazione di modifica JSON perché il valore originale di una colonna che memorizza i dati JSON viene passato come parametro.
Anche se questo è l’approcio corretto e i wrapper esistenti consentono la creazione di un tale UPDATE
statement, potrebbe non essere leggibile da un punto di vista del codice. Fortunatamente, Posjsonhelper ha un componente builder che rende semplice la costruzione di un tale statamento complesso.
Il tipo Hibernate6JsonUpdateStatementBuilder
consente la costruzione di istruzioni di aggiornamento con operazioni multiple che modificano il JSON e si basano l’una sull’altra.
Ecco un esempio di codice:
// 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\"}}");
Il comando SQL menzionato precedentemente è stato generato da questo codice.
Per sapere di più su come funziona il costruttore, consultare la documentazione.
Conclusione
La base di dati Postgres offre un vasto range di possibilità nell’operazione di modifica di dati JSON. Ciò ci porta a considerare Postgres come una buona scelta per il salvataggio di documenti. Quindi, se la nostra soluzione non richiede alte prestazioni in lettura, un migliore scalabilità o sharding (anche se tutte queste cose possono essere raggiunte con la base di dati Postgres, specialmente con soluzioni fornite da provider cloud come AWS), allora è degno di considerazione il salvataggio dei vostri documenti JSON in una base di dati Postgres — non menzionando il supporto per le transazioni offerto da basi di dati come Postgres.
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate