هذا مقال آخر في السلسلة المتعلقة بالدعم لل ما يليق بالمعايير الJSON للPostgres في مشروع يستخدم الإطار التوامي النسخة 6. الموضوع لهذا المقال هو عمليات التعديل على السجلات الJSON. وكما في المقال السابق، يستحق ذكر أن Postgres قد لا يمتلك هذه العمليات الشاملة كما البases الNoSQL الأخرى، مثل MongoDB لتعديل المعايير الJSON (على الرغم من أنه بالإنشاء الصحيح للوظائف، يمكن تحقيق نفس النتيجة). ومع ذلك، يتماشى مع دعم المتاتيريات (ولا دعم في قاعدة NoSQL بهذا المستوى). إذاً، فإن استخدام Postgres مع البيانات الJSON هو فكرة جيدة جدًا. بالطبع، توفر قواعد 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 الأصلية
مثلما في أخر مناظم الالجافا، يمكنك تنفيذ قوالب القوالب الSQL الأصلية — وهو موثوق ويوجد الكثير من الأمثلة على الإنترنت. لهذا السبب لن نركز في هذه المقالة على تنفيذ عمليات القوالب الSQL الأصلية. ومع ذلك سيكون هناك أمثلة عن أي نوع من القوالب الSQL التي تنتجها عمليات JPA. لأنه تحقيقة لمعايير JPA، يبدو من المنطقي أن نريح كيف يمكن تغيير بيانات الJSON في قاعدة Postgres بواسطة API JPA.
تغيير خصائص الجين الJSON وليس الجين الكامل (المسار)
تعيين جين البيانات الكامل لعمود واحد هو بسيط ولا يتطلب توضيح كثير. نحن فقط نضع قيمة لخصائص المملكة في صنف الكيان
الذي يمثل عمود مع JSON المحتويات.
هذا يشبه تعيين خصائص واحدة أو عدة خصائص للجين الJSON لسطر واحد فقط في الجدول. نقوم بقراءة سطر الجدول، تحليل القيمة الJSON إلى كيان تمثل جين المحتويات، تعيين قيم لخصائص معينة، وتحديث سجلات القاعدة بجين الكامل. ومع ذلك النهج، قد لا يكون من
لنفترض أن علينا أن نقوم بتحديثات جماعية لخصائص خاصة للJSON. جمع البيانات من القاعدة البيانات وتحديث كل سجل قد لا يكون طريقة فعالة.
سيكون أفضل قمامة بهذا التحديث بواحد عبارة update
حيث نحدد قيم الخصائص الخاصة للJSON. بالمصادفة، يوفر Postgres وظائف تغير المحتوى الخاص بالJSON ويمكن استخدامها في العبارة الSQL للتحديث.
Posjsonhelper
يوفر Hibernate دعم أفضل لتعديل الJSON في الإصدار 7، بما في ذلك معظم الوظائف والعملاء المذكورين في هذه المقالة. مع ذلك، لا يوجد خطط لإضافة هذا الدعم في الإصدار 6. بالمصادفة، مشروع Posjsonhelper يضيفدعملHibernate في الإصدار 6. جميع الأمثلة أدناه ستستخدم مكتبة Posjsonhelper.راقبهذاالرابط لمعرفة كيفية إلتزام مكتبة إلى مشروعك الجافا إسبيك..
تستخدم جميع الأمثلة فئة كيان Java التي تمثل جدول item
، والذي تم ذكر تعريفه أعلاه:
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 وعناصر محددة من المصفوفات استنادًا إلى فهرس المصفوفة.
على سبيل المثال، الكود أدناه يضيف الخاصية "birthday"
إلى الخاصية الداخلية "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());
سيولد هذا الكود بيان 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=?
لفافوظة عامل الربط “||”
تقوم اللفافة لعامل الربط (||
) بدمج قيمتين JSONB في قيمة JSONB جديدة.
استنادًا إلى وثائق Postgres، سلوك العامل هو كما يلي:
دمج مصفوفتين ينتج مصفوفة تحتوي على جميع عناصر كل إدخال. دمج كائنين ينتج كائنًا يحتوي على اتحاد مفاتيحهما، مع أخذ قيمة الكائن الثاني عند وجود مفاتيح مكررة. يتم معالجة جميع الحالات الأخرى عن طريق تحويل إدخال غير مصفوفة إلى مصفوفة ذات عنصر واحد، ثم المتابعة كما هو الحال بالنسبة لمصفوفتين. لا تعمل بشكل متكرر: يتم دمج هيكل المصفوفة أو الكائن على المستوى العلوي فقط.
إليك مثال على كيفية استخدام هذه اللفافة في الكود الخاص بك:
// 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 للعملية المتعلقة بالحذف (#-
). يحذف الحقل أو العنصر في الترتيب وفقاً للمسار المحدد، حيث يمكن أن تكون العناصر مفاتيح خاصة بالحقول أو أيام الترتيب. على سبيل المثال، سيأتي الأسلوب التالي يحذف من خصائص نظام البيانات ال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());
سيتم إنشاء القوالب 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;
وستستخدم وصلة واحدة من هذه الوظائف لتسمح لك بحذف عدد من القيم من ترتيب الJSON. هذا الكود يحذف "mask"
و "compass"
عناصر لخصائص "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\"]}");
هذه هي القوالب 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: كيفية تركيب عدة عمليات التعديل مع إحد
جميع الأمثلة السابقة عرضت تنفيذ عملية واحدة تغير بيانات ال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=?
هذا يفترض أن لدينا أربعة تنفيذات لـ jsonb_set function
وجميعاً تتكون من عدة 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