Dies ist ein weiterer Artikel in der Reihe, die sich mit der Unterstützung von Postgres-JSON-Funktionen in einem Projekt, das denHibernate-Framework mit Version 6 verwendet, beschäftigt. Der Artikel behandelt die Operationen der Modifizierung von JSON-Datensätzen. Wie im vorherigen Artikel erwähnt, hat Postgres möglicherweise keine umfassenden Operationen wie andere NoSQL-Datenbanken wie MongoDB für die JSON-Modifizierung (obwohl es mit den richtigen Funktionsbaukellen möglich ist, dasselbe Effekt zu erzielen). Es ist immer noch für die meisten Projekte geeignet, die JSON-Modifizierung benötigen. Plus, mit Transaktionsunterstützung (die ein NoSQL-Datenbank auf solch einem Niveau nicht bietet), ist es eine gute Idee, Postgres mit JSON-Daten zu verwenden. Natürlich bieten NoSQL-Datenbanken auch andere Vorteile, die für bestimmte Projekte besser geeignet sein könnten.
Es gibt im Allgemeinen viele Artikel über die Unterstützung von JSON durch Postgres. Der Artikel konzentriert sich auf die Integration dieser Unterstützung mit der Hibernate 6 Bibliothek.
Falls jemand an Interesse an der Abfrage von JSON-Daten oder dem Textsuchtengineering mit Postgres und Hibernate hat, ist er auf die unten stehenden Links aufmerksam zu halten:
Testdaten
Für das Artikel vermutet, dass unsere Datenbank eine Tabelle namens item
hat, die eine Spalte mit JSON-Inhalt hat, wie im untenstehenden Beispiel gezeigt:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
Wir könnten auch einige Testdaten haben:
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"}');
Native SQL-Ausführung
Wie in anderen Java-Frameworks, kann man mit Hibernate native SQL-Abfragen ausführen — was gut dokumentiert ist und es gibt viele Beispiele auf dem Internet. Daher werden wir in diesem Artikel nicht auf die Ausführung von nativem SQL konzentrieren. Allerdings werden Beispiele gegeben darüber, welche Art von SQL die JPA-Operationen generieren. Da Hibernate eine JPA-Implementierung ist, ist es sinnvoll zu zeigen, wie die JPA-API JSON-Daten in der Postgres-Datenbank ändern kann.
Verändere JSON-Objekt-Eigenschaften und nicht das gesamte JSON-Objekt (Pfad)
Es ist einfach, den gesamten JSON-Payload für eine Spalte zu setzen und erfordert keine detaillierte Erklärung. Wir setzen einfach den Wert für die Eigenschaft in unserer Entity
-Klasse, die eine Spalte mit JSON-Inhalt repräsentiert.
Es ist vergleichbar mit dem Setzen von einzelnen oder mehreren Eigenschaften für JSON für eine Datensatz in der Datenbank. Wir lesen einfach die Tabellenspalte, deserialisieren den JSON-Wert zu einem POJO, das ein JSON-Objekt repräsentiert, setzen Werte für bestimmte Eigenschaften und aktualisieren die Datenbankdatensätze mit dem gesamten Payload. Allerdings könnte diese Methode nicht praktisch sein, wenn wir JSON-Eigenschaften für mehrere Datensätze in der Datenbank ändern möchten.
Stellen wir uns vor, wir müssen eine Stapelaktualisierung von bestimmten JSON-Eigenschaften durchführen. Das Abrufen der Datenbank und die Aktualisierung jedes Datensatzes könnte keine wirklich effiziente Methode sein.
Es wäre deutlich besser, eine solche Aktualisierung mit einer einzigen update
-Anweisung durchzuführen, in der wir Werte für bestimmte JSON-Eigenschaften setzen. Glücklicherweise bietet Postgres Funktionen an, die den Inhalt von JSON verändern und in einer SQL-Aktualisierungsanweisung verwendet werden können.
Posjsonhelper
Hibernate bietet in Version 7 bessere Unterstützung für die JSON-Veränderung, einschließlich der meisten der in diesem Artikel erwähnten Funktionen und Operatoren. Trotzdem gibt es keine Pläne, solche Unterstützung in Version 6 hinzuzufügen. Glücklicherweise ergänzt das Posjsonhelper-Projekt diese Unterstützung für Hibernate in Version 6. Alle Beispiele, die unten gezeigt werden, werden die Posjsonhelper-Bibliothek verwenden.Schauen Sie auf diesen Link, um zu erfahren, wie Sie eine Bibliothek zu Ihrem Java-Projekt hinzufügen. Sie müssen auch den FunctionContributor attachen..
Alle Beispiele verwenden eine Java Entität Klasse, die die item
Tabelle repräsentiert, deren Definition oben erwähnt wurde:
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-Funktion Wrapper
Die jsonb_set
Funktion ist wahrscheinlich die am meisten hilfreiche Funktion, wenn es darum geht, JSON-Daten zu verändern. Sie erlaubt es, bestimmte Eigenschaften für JSON-Objekte und bestimmte Arrayelemente basierend auf dem Array-Index zu setzen.
Zum Beispiel fügt das folgende Codebeispiel der inneren Eigenschaft "child"
die Eigenschaft "birthday"
hinzu.
// 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());
Dieser Code generiert einen SQL-Befehl wie folgt:
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=?
Verbindungsoperator Wrapper „||“
Der Wrapper für den Verbindungsoperator (||
) verbindet zwei JSONB-Werte zu einem neuen JSONB-Wert.
Nach den Postgres Dokumentationen verhält sich der Operator wie folgt:
Verbinden von zwei Arrays erzeugt ein Array, das alle Elemente jedes Eingabearrays enthält. Verbinden von zwei Objekten erzeugt ein Objekt, das die Vereinigung ihrer Schlüssel enthält, wobei der zweite Wert bei duplizierten Schlüsseln verwendet wird. Alle anderen Fälle werden behandelt, indem ein nicht-arrayspezifischer Eingabewert in ein einzelnelementiges Array umgewandelt wird, und dann wie bei zwei Arrays fortgefahren wird. Operiert nicht rekursiv: Es wird nur die oberste Ebene des Array- oder Objekt-Struktur verknüpft.
Hier ist ein Beispiel, wie Sie diesen Wrapper in Ihrem Code verwenden können:
// 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, der ein JSON-Objekt mit der Eigenschaft child
mit dem bereits in der Datenbank gespeicherten JSON-Objekt zusammenführt.
Dieser Code generiert einen SQL-Aufruf wie folgt:
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=?
Felder oder Arrayelemente basierend auf der Indexposition an der angegebenen Pfad entfernen „#-„
Der Posjsonhelper verfügt über einen Wrapper für die Löschoperation (#-
). Er entfernt das Feld oder das Arrayelement basierend auf dem Index an der angegebenen Stelle, wobei die Stellenangaben entweder Feld Schlüssel oder Array-Indizes sein können. Zum Beispiel wird der untenstehende Code das JSON-Objekt auf Basis des JSON-Pfads "child.pets"
entfernen.
// 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());
Die generierte SQL-Anweisung würde lauten:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
Mehrere Arrayelemente an der angegebenen Pfad entfernen
Standardmäßig besitzt Postgres (mindestens in Version 16) keine integrierte Funktion, die Arrayelemente aufgrund ihres Werts löschen erlaubt. Allerdings besitzt es die integrierte Operation -#
, die wir oben erwähnt haben und die hilft, Arrayelemente auf der Basis ihres Index zu löschen, nicht jedoch aufgrund ihres Werts.
Zu diesem Zweck kann der Posjsonhelper eine Funktion generieren, die zu der Datenbank hinzugefügt und bei der DDL-Operation durchgeführt werden muss.
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;
Einer der Wrapper wird diese Funktion verwenden, um die Löschung mehrerer Werte aus dem JSON-Array zu ermöglichen. Dieser Code entfernt ein Element mit dem Schlüssel "mask"
und "compass"
für die Eigenschaft "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\"]}");
Hier ist die von obenstehendem Code generierte SQL:
update
item
set
jsonb_content=jsonb_set(jsonb_content, ?::text[], remove_values_from_json_array(jsonb_extract_path(jsonb_content, ?), ?::jsonb))
where
id=?
Hibernate6JsonUpdateStatementBuilder: Wie kann man mehrere Änderungsoperationen mit einer Update-Anweisung kombinieren?
Alle obigen Beispiele zeigten die Ausführung einer einzigen Operation, die JSON-Daten verändert. Natürlich können wir in unserem Code Update-Anweisungen haben, die mehrere der in diesem Artikel erwähnten Schrapper zusammen verwenden. Jedoch ist es wichtig zu wissen, wie diese Operationen und Funktionen ausgeführt werden, da es sinnvoll ist, wenn das Ergebnis der ersten JSON-Operation die Eingabe für die folgenden JSON-Veränderungsoperationen ist. Das Ergebnis dieser Operation würde die Eingabe für die nächste Operation sein und so fort, bis zur letzten JSON-Veränderungsoperation.
Um das zu verdeutlichen, schauen Sie auf den 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=?
Dies setzt voraus, dass wir vier jsonb_set function
-Ausführungen und zwei delete
-Operationen haben. Die tiefgestaffelte delete
-Operation ist eine erste JSON-Veränderungsoperation, weil der ursprüngliche Wert einer Spalte, die JSON-Daten speichert, als Parameter übergeben wird.
Obwohl dies die richtige Methode ist und der bestehende Schrapper die Erstellung solcher UPDATE
-Anweisungen zulässt, mag dies aus Code-Perspektive nicht lesbar sein. Glücklicherweise bietet Posjsonhelper einBuilder-Komponente, die die Erstellung solcher komplexen Anweisungen einfach macht.
Der Typ Hibernate6JsonUpdateStatementBuilder
ermöglicht die Erstellung von Update-Anweisungen mit mehreren Operationen, die JSON verändern und sich gegenseitig aufrufen.
Unten ist ein Codebeispiel:
// 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\"}}");
Der zuvor erwähnte SQL-Anweisung wurde durch diesen Code generiert.
Um mehr über die Arbeitsweise des Erstellers zu erfahren, bitte die Dokumentation anschauen.
Schlussfolgerung
Postgres-Datenbank bietet eine Vielzahl von Möglichkeiten für Operationen zur Bearbeitung von JSON-Daten. Dies veranlasst uns, Postgres als eine gute Wahl für die Dokumentenspeicherung zu betrachten. Also, wenn unsere Lösung keine höheren Lesepufferleistungen, bessere Skalierbarkeit oder Sharding-Funktionen (obwohl all dies mit der Postgres-Datenbank erreichbar ist, insbesondere mit Lösungen, die von Cloud-Anbietern wie AWS bereitgestellt werden) erfordert, dann ist es lohnenswert, darauf zu achten, Ihre JSON-Dokumente in einer Postgres-Datenbank zu speichern – ganz zu schweigen von der Transaktionsunterstützung durch Datenbanken wie Postgres.
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate