これは、 Postgres JSON関数のサポートに関連したシリーズの別の記事です。記事の主题は、JSONレコードの修正操作です。前記の記事と同様に、Postgresは他のNoSQLデータベースとして MongoDBなどに比べて、JSON修正にはそれほど完全な操作を持っていない可能性があることに留意してください(しかし、適切な関数構築を行うことで、同じ効果を得ることは可能です)。しかしながら、JSON修正が必要なほとんどのプロジェクトにはまだ適しています。また、トランザクションサポート(NoSQLデータベースのそのレベルではサポートしていません)があり、JSONデータを使用したPostgresを使用することは、とても良い選択です。もちろん、NoSQLデータベースには他にもより适したプロジェクトにとって優れた利点があります。
Postgresが JSONをサポートすることについては、一般的に多くの記事があります。この記事では、Hibernate 6ライブラリとこのサポートを統合することを焦点にしています。
JSONデータやテキスト検索にPostgresとHibernateを使用したクエリに興味を持つ人は、以下のリンクをご参照ください。
テストデータ
この記事のために、データベースにitem
というテーブルがあり、JSON形式のコラムがあることを想定します。以下の例のようにです。
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
また、テストデータもあります:
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"}');
ネイティブSQL実行
Javaフレームワークの他のものと同様に、HibernateではネイティブSQLクエリを実行できます。これは充実したドキュメントがあり、インターネット上に多くの例がありますので、この記事ではネイティブSQL操作の実行に焦点を当てません。しかし、JPA操作が生成するSQLの例を示す内容は含まれます。HibernateはJPA実装であり、JPA APIを使用してPostgresデータベース内のJSONデータを変更する方法を示すことは意味があります。
JSONオブジェクトの属性を変更し、JSONオブジェクト全体を変更しない(パス)
1つの列にJSONのすべてのデータを設定するのは簡単であり、多くの説明を必要としません。JSON内容の列を表現するEntity
クラスにある属性に値を設定するだけです。
1つのデータベース行にJSONの1つまたは複数の属性を設定するのも类似です。テーブル行を読み取り、JSON値をPOJOに Deserialize し、特定の属性に値を設定し、データベースレコードを更新し、整个のペイロードを適用します。しかし、複数のデータベース行のJSON属性を変更するためにJSON属性を設定する方法は、実用的ではありません。
假に特定のJSONプロパティに対する一括更新を行う必要があるとします。データベースからフェッチすることとそれぞれのレコードを更新することは、効果的な方法ではありません。
そのような更新を1つのupdate
ステートメントで行うことは、特定のJSONプロパティに値を設定するためにより良い方法です。幸运にも、PostgresはJSONコンテンツを変更する関数を持っていて、SQL更新ステートメントで使用できます。
Posjsonhelper
Hibernateには、この記事で提起された函数およびオペレータの多くによるJSON変更のサポートがバージョン7でより良くなっています。しかし、バージョン6にはそのようなサポートを追加する計画はありません。幸运にも、PosjsonhelperプロジェクトはHibernateのバージョン6にこのようなサポートを追加しています。以下のすべての例では、Posjsonhelperライブラリを使用します。FunctionContributor
すべての例は、上記に述べたitem
テーブルを表すJavaエンティティクラスを使用しています。
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 関数ラッパー
jsonb_set
関数は、JSONデータの修正が必要である場合におそらく最も有用な関数です。これにより、JSONオブジェクトの特定のプロパティと特定の配列要素に基づいて、配列インデックスによって設定することができます。
例えば、以下のコードは内部プロパティ"child"
にプロパティ"birthday"
を追加します。
// 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());
このコードは、以下のようなSQLステートメントを生成します。
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=?
結合演算子(||
)ラッパー
結合演算子(||
)のラッパーは、2つのJSONB値を新しいJSONB値に結合します。
Postgresの文書に基づいて、この演算子の動作は以下の通りです。
2つの配列を結合すると、各入力を含む全ての要素を持つ新しい配列が生成されます。2つのオブジェクトを結合すると、それぞれのキーのユニオンを持つ新しいオブジェクトが生成されます。重複するキーがある場合は、2番目のオブジェクトの値が使用されます。すべての他のケースは、非配列の入力を単一要素の配列に変換し、2つの配列として処理します。再帰的に操作しない:結合はただの阶层の配列またはオブジェクト構造のみを統合します。
このラッパーをコードで使用する例を以下に示します。
// 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());
このコードは、JSONオブジェクトとchild
プロパティを、データベースに既に保存されたJSONオブジェクトに結合します。
このコードは、以下のような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=?
指定したパスのインデックスに基づいてフィールドまたは配列要素を削除する#-
Posjsonhelperには、削除操作(#-
)のためのラッパーがあり、指定したパスのインデックスに基づいてフィールドまたは配列要素を削除します。パス要素はフィールドキーまたは配列インデックスとして指定できます。たとえば、以下のコードは、"child.pets"
JSONパスに基づいて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());
生成されるSQLは以下の通りです:
update
item
set
jsonb_content=(jsonb_content #- ?::text[])
where
id=?
指定したパスで複数の配列要素を削除する
デフォルトでは、Postgres(少なくともバージョン16で)配列要素の値に基づいて削除する内置関数を持っていません。しかし、上記に提及した内置オペレータ-#
が配列要素をインデックスに基づいて削除することができ、その値に基づいて削除することはできません。
このために、Posjsonhelperは、DDL操作に追加してデータベース上で実行する必要がある関数を生成することができます。
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;
そのようなラッパーの1つが、この関数を使用してJSON配列から複数の値を削除することを許可します。このコードは"child.inventory"
プロパティの"mask"
と"compass"
要素を削除します。
// 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\"]}");
上記のコードによって生成される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: 1つの更新ステートメントで複数の変更操作を結合する方法
上記のすべての例は、JSONデータを変更する单个の操作の実行を示しています。もちろん、この記事で触れたいろいろな包装器を複数使用した更新ステートメントをコードに書くこともできます。しかし、これらの操作や関数がどのように実行されるかを理解することは、最初のJSON操作の結果が次のJSON変更操作の入力として使われる場合に、最も意味のあることであることを認識するために非常に重要です。この操作の出力は次の操作の入力となり、その後、最後のJSON変更操作までになります。
これをより明确にするために、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=?
ここでは、4つのjsonb_set function
の実行と2つのdelete
操作を想定しています。最も内部的なdelete
操作は、JSONデータを格納する列の元の値がパラメータとして渡されるため、最初のJSON変更操作としてなされます。
これは正しい方法であり、既存の包装器はこのようなUPDATE
ステートメントの作成を可能にしていますが、コードの視点からは読みやすくありません。幸いにも、Posjsonhelperにはビルダーコンポーネントがあり、このような複雑なステートメントの構築を簡単にすることができます。
Hibernate6JsonUpdateStatementBuilder
型は、JSONを変更する複数の操作に基づいた更新ステートメントの構築を可能にし、互いに依存していることをサポートします。
以下は、このコードの例です。
// 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\"}}");
先程提起したSQLステートメントは、このコードによって生成されました。
ビルダーの機能について詳細に知るには、ドキュメントを確認してください。
結論
PostgresデータベースはJSONデータの修正操作について幅広い可能性を持っています。これにより、Postgresを良い文書保存解決策と考えることができます。なお、私たちの解決策が高い読み取りパフォーマンス、より良いスケーリング、または分割(これらはすべてPostgresデータベースで实现でき、特にAWSなどのクラウドプロバイダーから提供される解決策で)を必要としない場合、JSON文書をPostgresデータベースに格納することを考慮する価値があることは言うまでもありません。また、Postgresのようなデータベースとのトランザクションサポートも考慮すべきです。
Source:
https://dzone.com/articles/modify-json-data-in-postgres-and-hibernate