זה מאמר נוסף בסדרה שקשורה לתמיכה ב פונקציות JSON בPostgres בפרוייקט שמשתמש בשרשרת Hibernate עם הגירסה 6. הנושא של המאמר הוא פעולות העריכה על רשימות JSON. כמו במאמר הקודם, עלי לציין שPostgres עשוי להיות עכשיו ללא פעולות כה מורחבות כמו במסדרונות NoSQL אחרים כמו MongoDB לעריכת רשימות JSON (למרות זאת, עם היצירה הנכונה של פונקציות, אפשר להשיג את אותו האפקט). עדיין, זה מתאים לרוב הפרוייקטים שצריכים עריכת רשימות JSON. בנוסף, עם תמיכה בעסקה (שאינה קיימת במסדרון NoSQL ברמה כזאת), זה רעיון די טוב להשתמש בPostgres עם מידע JSON. כמובן, מסדרונות NoSQL מערכים ברמה אחרת שאולי טובים יותר עבור פרוייקטים.
ישנם בדרך כלל הרבה מאמרים על התמיכה בJSON בPostgres. המאמר הזה מתמקד בהתאמה הזו עם הספרה 6 של Hibernate.
במקרה שמישהו מעוניין בשאילת מידע 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 מקומי
כמו במערכות הג'אva האחרות בJava, אתם יכולים לבצע שאלות SQL מקומיות — שנועדו ופורסם טוב ויש המון דוגמאות ברשת. לכן במאמר הזה, לא נתמקד בביצועים של SQL המקומי. אך יהיו דוגמאות של איזה סוג של SQL הם יוצרים הפעלויות הJPA. בגלל שHibernate הוא היישום של JPA, מתברר שהגיוני להראות איך הAPI של JPA יכול לשנות מידע JSON במסד הנתונים של Postgres.
שינוי תכונות מושגים בעלי הערך בלי לשנות את כל הערך הJSON כולו (מסלול)
הגדלת כל אובייקט הערך JSON לשורה אחת במסד הנתונים קלה ולא דורשת הרבה הסבר. אנחנו פשוט מגדירים את הערך עבור התכונה במקום זה במשך המשך המערכת Entity
שמייצגת את העמודה עם תוכן JSON.
זה דומה לגדלת תכונות אחדות או מספר תכונות בערך JSON עבור שורה אחת בטבלה. אנחנו פשוט קוראים את השורה הזו, מהפך את הערך JSON לאובייקט פו'קו שמייצג אובייקט JSON, משנים ערכים לתכונות מסוימות, ומעדכנים את השיר
נניח שעלינו לבצע עדכונים מוצקים בתכונות JSON מסויימות. להוצאה מהבסיס המחשבה ועדכון כל רשומה אולי לא די יעיל.
זה יהיה הרבה יותר טוב לבצע עדכון כך בעזרת הציון update
בו אנחנו מגדירים ערכים לתכונות JSON מסויימות. למרבה המזל, פוסגרס מקבל פונקציות שמשנות תוכן JSON ואלה יכולים להשתמש בהם במשוב SQL על מנת עדכון.
Posjsonhelper
היברנט מקבל תמיכה טובה יותר בשינויים בJSON בגרסה 7, כולל רוב הפונקציות והמפעילים המוצגים בזה המאמר. עדיין, אין תכניות להוסיף תמיכה כזו בגרסה 6. למרבה המזל, הפרוייקט Posjsonhelper מוסיף את התמיכה להיברנט בגרסה 6. כל הדוגמאות למטה תשתמשו בספריית Posjsonhelper.בחץ על קישור זה כדי למצוא איך לחבר ספרייה לפרוייקט הגיוני שלך. תהיה צריך גם לחבר FunctionContributor.
כל הדוגמאות משתמשות במחלקת יישות 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;
אחד המעטפים ישתמש בפונקציה זו על מנת לאפשר מסירת רבים מהערכים מרשימת הרשימה. הקוד הזה מסיר "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
. הפעולה הכי מעורבת 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