זהו המשך של המאמר הקודם בו תיארו כיצד להוסיף תמיכה בפונקציות JSON של Postgres ולהשתמש ב-Hibernate 5. במאמר זה, נתמקד באיך להשתמש בפעולות JSON בפרויקטים שמשתמשים ב-מסגרת Hibernate עם גרסה 6.
תמיכה מקורית
ל-Hibernate 6 כבר יש תמיכה טובה בתחביר של שאילתות לפי תכונות JSON כפי שמוצג בדוגמה הבאה:
יש לנו את המחלקה הגופנית הרגילה שיש לה תכונה אחת של JSON:
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;
@Entity
@Table(name = "item")
public class Item implements Serializable {
@Id
private Long id;
@JdbcTypeCode(SqlTypes.JSON)
@Column(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;
}
}
סוג JsonbContent
נראה כך:
import jakarta.persistence.Embeddable;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;
import org.hibernate.annotations.Struct;
import java.io.Serializable;
import java.util.List;
@Embeddable
public class JsonbContent implements Serializable{
private Integer integer_value;
private Double double_value;
@Enumerated(EnumType.STRING)
private UserTypeEnum enum_value;
private String string_value;
//Getters ו-Setters
}
כשיש לנו מודל כזה אפשר לשאול, למשל, לפי התכונה string_value
.
public List<Item> findAllByStringValueAndLikeOperatorWithHQLQuery(String expression) {
TypedQuery<Item> query = entityManager.createQuery("from Item as item_ where item_.jsonbContent.string_value like :expr", Item.class);
query.setParameter("expr", expression);
return query.getResultList();
}
חשוב! – כרגע, נראה שיש כמה מגבלות עם התמיכה בשאילתות לפי תכונות שכן אנו לא יכולים לשאול לפי סוגים מורכבים כמו מערכים. כפי שאפשר לראות, סוג JsonbContent
מכיל את הערך Embeddable
מה שאומר שאם תנסה להוסיף תכונה שהיא רשימה אנו עשויים לראות שגיאה עם הודעה הבאה: הסוג שאמור להיות מסונכרן כ-JSON לא יכול להכיל סוגים מורכבים כמו תכונות: רכיבים אגרטיביים כרגע יכולים להכיל רק ערכים בסיסיים פשוטים ורכיבים של ערכים בסיסיים פשוטים.
במקרה שבו הסוג JSON שלנו אינו זקוק לתכונות עם סוג מורכב, אזי התמיכה המקורית מספיקה.
נא לבדוק את הקישורים להלן לקבלת מידע נוסף:
- Stack Overflow: Hibernate 6.2 וניווט JSON
- Hibernate ORM 6.2 – המיפויים המרכזיים המורכבים
- GitHub: hibernate6-tests-native-support-1
עם זאת, לעתים קרובות כדאי להחזיק ביכולת לשאול לפי תכונות מערך. כמובן, שאנו יכולים להשתמש בשאילתות SQL מקוריות ב-Hibernate ולהשתמש בפונקציות JSON של Postgres שהוצגו במאמר הקודם. אבל זה יהיה גם שימושי להחזיק ביכולת זו בשאילתות HQL או כשמשתמשים בפרדיקטים באופן תכנותי. הגישה השנייה זהה יותר שימושית כשאתה אמור ליישם את הפונקציות של שאילתה דינמית. אם כי צירוף מחרוזת דינמי שאמור להיות שאילתת HQL עשוי להיות קל, אך הנוהג הטוב יותר יהיה להשתמש בפרדיקטים שהוגדרו. כאן השימוש בספרייה posjsonhelper הופך לשימושי.
Posjsonhelper
הפרויקט קיים במרכז האחסון Maven של Maven, כך שאפשר להוסיף אותו בקלות על ידי הוספתו כתלות בפרויקט Maven שלך.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate6</artifactId>
<version>0.2.1</version>
</dependency>
רישום FunctionContributor
כדי להשתמש בספרייה, עלינו להקים את הרכיב FunctionContributor
. אפשר לעשות זאת בשתי דרכים. הדרך הראשונה והמומלצת ביותר היא ליצור קובץ בשם org.hibernate.boot.model.FunctionContributor תחת המדריך resources/META-INF/services.
כתוכן של הקובץ, פשוט הכנס את השימוש בposjsonhelper
של סוג org.hibernate.boot.model.FunctionContributor
.
com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor
הפתרון החלופי הוא להשתמש ברכיב com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher
במהלך ההתחלה של היישום, כפי שמוצג בדוגמה להלן עם השימוש במסגרת ה-Spring.
import com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher;
import jakarta.persistence.EntityManager;
import org.hibernate.query.sqm.NodeBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
@Configuration
public class FunctionDescriptorConfiguration implements
ApplicationListener<ContextRefreshedEvent> {
@Autowired
private EntityManager entityManager;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
NodeBuilder nodeBuilder = (NodeBuilder) entityManager.getCriteriaBuilder();
SqmFunctionRegistryEnricher sqmFunctionRegistryEnricher = new SqmFunctionRegistryEnricher();
sqmFunctionRegistryEnricher.enrich(nodeBuilder.getQueryEngine().getSqmFunctionRegistry());
}
}
לקבלת פרטים נוספים, נא לבדוק "כיצד להקים FunctionContributor".
דוגמה למודל
המודל שלנו נראה כמו הדוגמה להלן:
package com.github.starnowski.posjsonhelper.hibernate6.demo.model;
import io.hypersistence.utils.hibernate.type.json.JsonType;
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;
@Entity
@Table(name = "item")
public class Item {
@Id
private Long id;
@JdbcTypeCode(SqlTypes.JSON)
@Type(JsonType.class)
@Column(name = "jsonb_content", columnDefinition = "jsonb")
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;
}
}
חשוב!: בדוגמה זו, הנכס JsonbConent
הוא סוג מUSTOM (כפי שמוצג להלן), אך ייתכן שזה יהיה גם סוג מחרוזת.
package com.github.starnowski.posjsonhelper.hibernate6.demo.model;
import jakarta.persistence.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.io.Serializable;
import java.util.List;
public class JsonbContent implements Serializable{
private List top_element_with_set_of_values;
private Integer integer_value;
private Double double_value;
@Enumerated(EnumType.STRING)
private UserTypeEnum enum_value;
private String string_value;
private Child child;
// קובעים ומקבלים
}
פעולות DDL עבור השולחן:
create table item (
id bigint 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"]}');
INSERT INTO item (id, jsonb_content) VALUES (3, '{"top_element_with_set_of_values":["TAG1","TAG3"]}');
INSERT INTO item (id, jsonb_content) VALUES (4, '{"top_element_with_set_of_values":["TAG22","TAG21"]}');
INSERT INTO item (id, jsonb_content) VALUES (5, '{"top_element_with_set_of_values":["TAG31","TAG32"]}');
-- פריט ללא תכונות, רק json ריק
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');
-- ערכים int
INSERT INTO item (id, jsonb_content) VALUES (7, '{"integer_value": 132}');
INSERT INTO item (id, jsonb_content) VALUES (8, '{"integer_value": 562}');
INSERT INTO item (id, jsonb_content) VALUES (9, '{"integer_value": 1322}');
-- ערכים double
INSERT INTO item (id, jsonb_content) VALUES (10, '{"double_value": 353.01}');
INSERT INTO item (id, jsonb_content) VALUES (11, '{"double_value": -1137.98}');
INSERT INTO item (id, jsonb_content) VALUES (12, '{"double_value": 20490.04}');
-- ערכים enum
INSERT INTO item (id, jsonb_content) VALUES (13, '{"enum_value": "SUPER"}');
INSERT INTO item (id, jsonb_content) VALUES (14, '{"enum_value": "USER"}');
INSERT INTO item (id, jsonb_content) VALUES (15, '{"enum_value": "ANONYMOUS"}');
-- ערכים מחרוזת
INSERT INTO item (id, jsonb_content) VALUES (16, '{"string_value": "this is full sentence"}');
INSERT INTO item (id, jsonb_content) VALUES (17, '{"string_value": "this is part of sentence"}');
INSERT INTO item (id, jsonb_content) VALUES (18, '{"string_value": "the end of records"}');
-- רכיבים פנימיים
INSERT INTO item (id, jsonb_content) VALUES (19, '{"child": {"pets" : ["dog"]}}');
INSERT INTO item (id, jsonb_content) VALUES (20, '{"child": {"pets" : ["cat"]}}');
INSERT INTO item (id, jsonb_content) VALUES (21, '{"child": {"pets" : ["dog", "cat"]}}');
INSERT INTO item (id, jsonb_content) VALUES (22, '{"child": {"pets" : ["hamster"]}}');
באמצעות רכיבי קריטריונים
להלן דוגמה לאותה שאילתה המוצגת בתחילת המאמר, אך נוצרה באמצעות רכיבי SQM ובניית קריטריונים:
public List<Item> findAllByStringValueAndLikeOperator(String expression) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> root = query.from(Item.class);
query.select(root);
query.where(cb.like(new JsonBExtractPathText(root.get("jsonbContent"), singletonList("string_value"), (NodeBuilder) cb), expression));
return entityManager.createQuery(query).getResultList();
}
היברנייט הולך לייצר את קוד ה-SQL כדלקמן:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''
הפונקציה jsonb_extract_path_text
היא פונקציה של Postgres השקולה לאופרטור #>>
(אנא בדוק את תיעוד Postgres הקשור קודם לכן לפרטים נוספים).
פעולות על מערכים
הספרייה תומכת בכמה פונקציות ואופרטורים של Postgres לגבי JSON, כגון:
?&
– זה בודק האם כל המחרוזות במערך הטקסט קיימות כמפתחות ראשיים או רכיבים במערך. בדרך כלל, אם יש לנו תכונת JSON המכילה מערך, אז אפשר לבדוק האם היא מכילה את כל האלמנטים שאנו מחפשים.?|
– זה בודק האם כל המחרוזות במערך הטקסט קיימות כמפתחות ראשיים או רכיבים במערך. בדרך כלל, אם יש לנו תכונת JSON המכילה מערך, אז אפשר לבדוק האם היא מכילה לפחות את האלמנטים שאנו מחפשים.
חוץ מביצוע שאילתות SQL מקוריות, ל-Hibernate 6 אין תמיכה בפעולות שמתוארות לעיל.
שינויים חובה בDDL
המפעיל לעיל לא יכול לשמש בHQL בגלל תווים מיוחדים. זו הסיבה שעלינו לאגור אותם, למשל, בפונקציה SQL מותאמת אישית. הספרייה Posjsonhelper
דורשת שתי פונקציות SQL מותאמות אישית המאגרות את המפעילים הללו. עבור ההגדרה הברירת מחדל, הפונקציות הללו יש את היישום להלן.
CREATE OR REPLACE FUNCTION jsonb_all_array_strings_exist(jsonb, text[]) RETURNS boolean AS $$
SELECT $1 ?& $2;
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION jsonb_any_array_strings_exist(jsonb, text[]) RETURNS boolean AS $$
SELECT $1 ?| $2;
$$ LANGUAGE SQL;
לקבלת מידע נוסף על איך להתאים או להוסיף באופן מבוסס תוכנה את ה-DDL הנחוץ, נא לבדוק את הקטע "יישום שינויים בDDL."
"?&" מעטפת
הדוגמה לקוד להלן מדגימה איך ליצור שאילתה העוסקת ברשומות שבהן תכונת JSON מכילה מערך שכל האלמנטים המחרוזתיים בו אנו משתמשים בחיפוש.
public List<Item> findAllByAllMatchingTags(Set<String> tags) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> root = query.from(Item.class);
query.select(root);
query.where(new JsonbAllArrayStringsExistPredicate(hibernateContext, (NodeBuilder) cb, new JsonBExtractPath(root.get("jsonbContent"), (NodeBuilder) cb, singletonList("top_element_with_set_of_values")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
במקרה שהתגים מכילים שני אלמנטים, אז Hibernate יפיק את ה-SQL להלן:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_all_array_strings_exist(jsonb_extract_path(i1_0.jsonb_content,?),array[?,?])
"?|" מעטפת
הקוד בדוגמה להלן מדגימה איך ליצור שאילתה העוסקת ברשומות שבהן תכונת JSON מכילה מערך ויש לפחות אלמנט מחרוזתי אחד שאנו משתמשים בו בחיפוש.
public List<Item> findAllByAnyMatchingTags(HashSet<String> tags) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> root = query.from(Item.class);
query.select(root);
query.where(new JsonbAnyArrayStringsExistPredicate(hibernateContext, (NodeBuilder) cb, new JsonBExtractPath(root.get("jsonbContent"), (NodeBuilder) cb, singletonList("top_element_with_set_of_values")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
במקרה שהתגים מכילים שני אלמנטים, אז Hibernate יפיק את ה-SQL להלן:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_any_array_strings_exist(jsonb_extract_path(i1_0.jsonb_content,?),array[?,?])
לקבלת דוגמאות נוספות על איך להשתמש במפעילים מספריים נא לבדוק את הדגמים של אובייקט dao ו-בדיקות dao.
למה להשתמש בספריית posjsonhelper כשלהייברנייט יש תמיכה מספרית בתכונות JSON
חוץ משני המפעילים המתמודדים עם סוגי המערכים שהוזכרו לעיל, לספרייה יש עוד שני מפעילים שימושיים. <קוד>jsonb_extract_path ו-<קוד>jsonb_extract_path_text הם ערימות עבור המפעילים <קוד>#> ו-<קוד>#>>. להייברנייט תמיכה במפעיל <קוד>->>. כדי לראות את ההבדל בין המפעילים אלו, נא לבדוק את התיעוד של פוסטגרס שקישרנו למוקדם יותר.
עם זאת, כפי שקראת בתחילת המאמר, התמיכה בשאילתות מקוריות לתכונות JSON מותרת רק כאשר המחלקה JSON מכילה תכונות עם סוגים פשוטים. והכי חשוב, אינך יכול לבדוק לפי תכונה אם היא אינה מוקמת לתכונה בסוג ה-JSON. זה עשוי להיות בעיה אם אתה מניח שמבנה ה-JSON שלך יכול להיות דינאמי יותר ויש לו מבנה גמיש לא מוגדר על ידי שום תבנית.
עם המפעיל posjsonhelper
, אין לך בעיה זו. אתה יכול לבדוק לפי כל תכונה שתרצה. אין צורך שיוגדר כתכונה בסוג JSON. יתרה מזו, הפריט ביעילות שלנו שאוגר את עמודת JSON אינו חייב להיות אובייקט מורכב כמו JsonbConent
בדוגמאות שלנו. זה יכול להיות מילה פשוטה ב-Java.
מסקנה
כפי שצוין במאמר הקודם, במקרים מסוימים, סוגי JSON ופונקציות Postgres יכולות להוות אלטרנטיבות טובות למאגרי נתונים NoSQL. זה יכול לחסוך לנו מהחלטה להוסיף פתרונות NoSQL לעמדת הטכנולוגיה שלנו שיכולה גם להוסיף יותר מורכבות ועלויות נוספות.
זה גם מאפשר לנו גמישות כשאנו זקוקים לאחסון נתונים לא מובנים בבסיס היחס שלנו והאפשרות לבדוק במבנים אלה.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6