Dit is een ander artikel in de reeks die gerelateerd is aan het ondersteunen van Postgres JSON-functies in een project dat de Hibernate framework gebruikt met versie 6. Het thema voor het artikel is de aanpassingsbewerkingen op JSON-records. Net zoals in het vorige artikel, is het waard om te vermelden dat Postgres misschien nu zo uitgebreide bewerkingen als andere NoSQL-databassen zoals MongoDB voor JSON-bewerking heeft ( hoewel het met de juiste functieconstructies mogelijk is om hetzelfde effect te behalen). Het is nog steeds geschikt voor de meeste projecten die JSON-bewerking nodig hebben. Bovendien, met ondersteuning voor transacties ( die niet beschikbaar is in een NoSQL-databank op zo’n niveau), is het een goed idee om Postgres te gebruiken met JSON-gegevens. Natuurlijk hebben NoSQL-databassen andere voordelen die misschien beter aan projecten passen.
Er zijn er in het algemeen veel artikelen over de ondersteuning vanJSON door Postgres. Dit artikel focus op het integreren van deze ondersteuning met de Hibernate 6-bibliotheek.
Voor wie geïnteresseerd is in het opsporen van JSON-gegevens of tekstzoeken met behulp van Postgres en Hibernate, zie de onderstaande koppelingen:
Testgegevens
Voor het artikel, stel dat onze database een tabel heet genaamd item
heeft, die een kolom bevat met JSON-inhoud, zoals in het onderstaande voorbeeld:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
We hebben misschien ook enkele testgegevens:
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"}');
Natieve SQL-uitvoering
Net zoals in andere Java-frameworks, kan je met Hibernate ook native SQL-query’s uitvoeren — wat goed gedocumenteerd is en veel voorbeelden op het internet te vinden zijn. Daarom zal dit artikel niet focus leggen op de uitvoering van native SQL-operaties. Er zal echter wel een voorbeeld zijn van welke soort SQL de JPA-operaties genereren. want Hibernate is een JPA-implementatie, het maakt zin om te laten zien hoe de JPA-API JSON-gegevens in de Postgres-database kan wijzigen.
Wijzigen van JSON-objecteigenschappen en niet het gehele JSON-object (pad)
Het instellen van het gehele JSON-payload voor één kolom is gemakkelijk en vereist niet veel uitleg. We stellen de waarde voor de eigenschap in onze Entity
-klasse, die een kolom met JSON-inhoud representeert.
Het is vergelijkbaar met het instellen van een enkele of meerdere eigenschappen voor JSON voor één databaseregel. We lezen gewoon de tabelregel in, deserialiseren de JSON-waarde naar een POJO dat een JSON-object weergeeft, stellen waarden voor bepaalde eigenschappen in en update de databaserecords met het gehele payload. Het is echter misschien niet praktisch als we JSON-eigenschappen willen wijzigen voor meerdere databaseregels.
Veronderstel dat we een batch-update moeten uitvoeren op bepaalde JSON-eigenschappen. Het halen van informatie uit de database en het bijwerken van elk record kan niet de effectiefste methode zijn.
Het zou veel beter zijn om zo’n update uit te voeren met één update
-instructie waarin we waarden instellen voor bepaalde JSON-eigenschappen. gelukkig heeft Postgres functies die JSON-inhoud wijzigen en die kunnen worden gebruikt in de SQL-update-instructie.
Posjsonhelper
Hibernate biedt betere ondersteuning voor JSON-bewerking aan vanaf versie 7, inclusief de meeste functies en operators die in dit artikel worden genoemd. Toch zijn er geen plannen om zo’n ondersteuning toe te voegen in versie 6. Gelukkig biedt het Posjsonhelper-projectdergelijkeondersteuningaan voor Hibernate in versie 6. Alle voorbeelden hieronder zullen de Posjsonhelper-bibliotheek gebruiken.Bekijk dezelink om te leren hoe u een bibliotheek aan uw Java-project kunt koppelen. U zal ook moeten koppelen FunctionContributor.
Alle voorbeelden gebruiken een Java entity klasse die de tabel item
weergeeft, waarvan de definitie hierboven werd genoemd:
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
De functie jsonb_set
is waarschijnlijk de meest nuttige functie bij het aanpassen van JSON gegevens nodig. Het stelt JSON objecten en specifieke array elementen beschikbaar op basis van de array index.
Bijvoorbeeld zorgt het onderstaande code ervoor dat de eigenschap "birthday"
aan de binnenliggende eigenschap "child"
wordt toegevoegd.
// 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());
Deze code zou de volgende SQL-instructie genereren:
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=?
Concatenatie Operator Wrapper “||”
De wrapper voor de concatenatie operator (||
) concateneert twee JSONB waarden tot een nieuwe JSONB waarde.
Volgens de Postgres documentatie, gedraait het operator gedrag als volgt:
Concatenatie van twee array’s genereert een array dat alle elementen van elke invoer bevat. Concatenatie van twee objecten genereert een object dat de union bevat van hun sleutels, neemt de waarde van het tweede object over bij het optreden van dubbele sleutels. Alle andere gevallen worden behandeld door een niet-array invoer in een array met een enkel element om te zetten, en vervolgens wordt de behandeling als bij twee array’s doorgevoerd. Wordt niet recursief uitgevoerd: alleen de bovenste array of object structuur wordt samengevoegd.
Hier is een voorbeeld van hoe u deze wrapper in uw code kunt gebruiken:
// 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());
Code die een JSON object met de eigenschap child
samenvoegt met het al opgeslagen JSON object in de database.
Deze code genereert zo’n SQL-query:
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=?
Verwijder het veld of array-element op basis van de index op het opgegeven pad “#-“
De Posjsonhelper heeft een wrapper voor de delete-bewerking (#-
). Het verwijdert het veld of array element gebaseerd op de index op het gespecificeerde pad, waar pad elementen ofwel veld sleutels of array indexen kunnen zijn. De onderstaande code verwijdert bijvoorbeeld de eigenschap van het JSON-object op basis van het JSON-pad "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());
De gegenereerde SQL zou zijn:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
Verschrap meerdere array-elementen op het opgegeven pad
Standaard heeft Postgres (tenminste in versie 16) geen ingebouwde functie waarmee array-elementen kunnen worden verwijderd op basis van hun waarde. Postgres heeft echter wel de ingebouwde operator -#
, die we hierboven hebben genoemd, waarmee array-elementen kunnen worden verwijderd op basis van de index, maar niet op basis van hun waarde.
De Posjsonhelper kan hiervoor een functie genereren die moet worden toegevoegd aan de DDL-bewerking en moet worden uitgevoerd op je 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;
Eén van de wrappers gebruikt deze functie om meerdere waarden uit de JSON-array te verwijderen. Deze code verwijdert een "mask"
en "compass"
elementen voor de "child.inventory"
eigenschap.
// 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\"]}");
Hier is de SQL die wordt gegenereerd door de bovenstaande 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: Hoe meerdere wijzigingsbewerkingen combineren met één updatestatement
Alle bovenstaande voorbeelden tonen de uitvoering van een enkele bewerking die JSON-gegevens wijzigt. Natuurlijk kunnen we in ons code update-declaraties hebben die samen met veel van de beschermers genoemd in dit artikel worden gebruikt. Het is echter belangrijk op de hoogte te zijn van hoe deze bewerkingen en functies zullen worden uitgevoerd, omdat dit het meest logisch is als het resultaat van de eerste JSON-bewerking de invoer is voor de volgende JSON-wijzigingsbewerkingen. Het uitvoeringsresultaat van deze bewerking zou de invoer zijn voor de volgende bewerking, enzovoort, tot de laatste JSON-wijzigingsbewerking.
Om dit beter te illustreren, kijk je naar de SQL-code.
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=?
Dit gaat ervan uit dat we vier keer jsonb_set function
worden uitgevoerd en twee delete
bewerkingen. De meest ingevoerde delete
bewerking is de eerste JSON-wijzigingsbewerking, want de originele waarde uit een kolom die JSON-gegevens opslaat, wordt als parameter meegegeven.
Hoewel dit de correcte aanpak is, en de bestaande omhuller de mogelijkheid biedt om zo’n UPDATE
-declaratie te maken, zou het misschien niet vanuit het perspectief van de code leesbaar zijn. gelukkig heeft Posjsonhelper een builder-component die het makkelijker maakt om zo’n complexe declaratie te bouwen.
Het type Hibernate6JsonUpdateStatementBuilder
maakt het mogelijk om update-declaraties te bouwen met meerdere bewerkingen die JSON wijzigen en elkaar afhankelijk zijn.
Hieronder volgt een codevoorbeeld:
// 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\"}}");
De eerder genoemde SQL-declaratie werd door deze code gegenereerd.
Voor meer informatie over hoe de builder werkt, kijk dan eens naar de documentatie.
Conclusie
De Postgres database biedt veel mogelijkheden op het gebied van JSON-gegevensmodificaties. Dit maakt Postgres een goede keuze voor een documentenopslagoplossing. Dus, als onze oplossing geen hogere leesprestaties, betere schaalbaarheid of sharding vereist (hoewel alles dit met de Postgres database kan worden gerealiseerd, vooral met oplossingen aangeboden door cloudproviders zoals AWS), dan is het waard om aan te nemen dat uw JSON-documenten in de Postgres database opgeslagen kunnen worden — niet vergeleken met de ondersteuning van transacties met databases zoals Postgres.
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate