פונקציות JSON של Postgres עם Hibernate 5

מסד נתונים פוסטרגס תומך בכמה סוגים של JSON ופעולות מיוחדות עבור סוגים אלה.

במקרים מסוימים, פעולות אלה עשויות להוות אלטרנטיבה טובה למסדי נתונים דוגמניים כמו MongoDB או מסדי נתונים NoSQL אחרים. כמובן, מסדי נתונים כמו MongoDB אולי יהיו בעלי תהליכי שכפול טובים יותר, אך הנושא הזה נמצא מחוץ לטווח של המאמר הזה.

במאמר זה, נתמקד באופן שימוש בפעולות JSON בפרויקטים המשתמשים במסד קוד היפרניט עם גרסה 5.

דוגמא מודל

המודל שלנו נראה כמו הדוגמא להלן:

Java

 

@Entity
@Table(name = "item")
public class Item {

    @Id
    private Long id;

    @Column(name = "jsonb_content", columnDefinition = "jsonb")
    private String jsonbContent;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getJsonbContent() {
        return jsonbContent;
    }

    public void setJsonbContent(String jsonbContent) {
        this.jsonbContent = jsonbContent;
    }
}

חשוב!: יכולנו להשתמש בסוג JSON ספציפי עבור הנכס jsonbContent, אך בגרסה 5 של היפרניט, זה לא יספק יתרונות מבחינת פעולות.

פעולת DDL:

SQL

 

create table item (
       id int8 not null,
        jsonb_content jsonb,
        primary key (id)
    )

לצורך ההדגמה, נניח שבמסד הנתונים שלנו נמצאות רשומות כאלה:

SQL

 

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"]}');

-- פריט ללא תכונות, רק עם ג'ייסון ריק
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"]}}');

גישה לשאילתה מקורית

ב-Hibernate 5, אנו יכולים להשתמש בגישה מקורית שם אנו מביצעים פקודת SQL ישירה.

חשוב!: בבקשה, למטרות הצגה, השתיקת העובדה שהקוד להלן מאפשר הזרמת SQL עבור הביטוי לפעולה LIKE. כמובן, לפעולה כזו, עלינו להשתמש בפרמטרים ובPreparedStatement.

Java

 


private EntityManager entityManager;

public List<Item> findAllByStringValueAndLikeOperatorWithNativeQuery(String expression) {
        return entityManager.createNativeQuery("SELECT * FROM item i WHERE i.jsonb_content#>>'{string_value}' LIKE '" + expression + "'", Item.class).getResultList();
    }

בדוגמה לעיל, משתמשים בפעולה #>> שמוציאה את האובייקט התת-ג'ייסון בנתיב המסומן כטקסט (אנא בדוק את תיעוד Postgres לפרטים נוספים).

ברוב המקרים, שאילתה כזו (כמובן, עם ערך מרוקן) תספיק. עם זאת, אם אנו זקוקים ליישום יצירת שאילתה דינמית מסוג כלשהו על פי פרמטרים שנשלחו ב-API שלנו, יהיה טוב יותר להשתמש במבנה קריאה מסוג כלשהו.

Posjsonhelper

Hibernate 5 ללא תמיכה באופן כפוף בפונקציות ג'ייסון של Postgres. למרבה המזל, אפשר ליישם זאת בעצמך או להשתמש בספרייה posjsonhelper שהיא פרויקט פתוח מקורי.

הפרויקט קיים במרכזי Maven, כך שתוכל בקלות להוסיף אותו על ידי הוספתו כתלוייה לפרויקט Maven שלך.

XML

 

        <dependency>
            <groupId>com.github.starnowski.posjsonhelper</groupId>
            <artifactId>hibernate5</artifactId>
            <version>0.1.0</version>
        </dependency>

כדי להשתמש בספריית posjsonhelper בפרויקט שלך, תצטרך להשתמש בדיאלקט של Postgres המיושם בפרויקט. לדוגמה:

com.github.starnowski.posjsonhelper.hibernate5.dialects.PostgreSQL95DialectWrapper ...

במידה ולפרויקט שלך כבר יש מחלקת דיאלקט מותאמת אישית, ישנה גם אפשרות להשתמש:

com.github.starnowski.posjsonhelper.hibernate5.PostgreSQLDialectEnricher;

ברכיבי קריטריונים

הדוגמה להלן מתנהגת באופן דומה לדוגמה הקודמת שהשתמשה בשאילתה מקורית. עם זאת, במקרה זה, אנו הולכים להשתמש בבנאי קריטריונים.

Java

 

	private EntityManager entityManager;

    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((CriteriaBuilderImpl) cb, singletonList("string_value"), root.get("jsonbContent")), expression));
        return entityManager.createQuery(query).getResultList();
    }

Hibernate הולך לייצר את הקוד SQL כדלקמן:

SQL

 

select
            item0_.id as id1_0_,
            item0_.jsonb_content as jsonb_co2_0_ 
        from
            item item0_ 
        where
            jsonb_extract_path_text(item0_.jsonb_content,?) like ?

הפונקציה jsonb_extract_path_text היא פונקציה של Postgres השקולה לאופרטור #>> (אנא בדוק את תיעוד Postgres הקשור קודם לכן לפרטים נוספים).

פעולות על מערכים

הספריה תומכת בכמה פונקציות ואופרטורים של Postgres JSON כמו:

  • ?& – בודק אם כל המחרוזות במערך הטקסט קיימות כמפתחות ראשיים או רכיבים במערך. אז בדרך כלל אם יש לנו תכונת JSON שמכילה מערך, אז אפשר לבדוק אם היא מכילה את כל האלמנטים שאנו מחפשים.
  • ?| – בודק אם כל המחרוזות במערך הטקסט קיימות כמפתחות ראשיים או רכיבים במערך. אז בדרך כלל אם יש לנו תכונת JSON שמכילה מערך, אז אפשר לבדוק אם היא מכילה לפחות אלמנט אחד מהאלמנטים שאנו מחפשים.

השינויים הדרושים בDDL

המפעיל לעיל לא יכול לשמש ב-HQL בגלל תווים מיוחדים. זו הסיבה שעלינו לאפשר להם להכנס, למשל, בתוך פונקציה SQL מותאמת אישית. ספריית Posjsonhelper דורשת שתי פונקציות SQL מותאמות אישית המאריזות את המפעילים. עבור ההגדרה הברירת מחדל הפונקציות יש את היישום למטה.

PLSQL

 

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 המכילה מערך כולל אלמנטים מחרוזת שאנו מחפשים עליהם.

Java

 

    
	private EntityManager entityManager;

	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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
        return entityManager.createQuery(query).getResultList();
    }

במקרה שהתגים יכילו שני אלמנטים, אז Hibernate היה מייצר את ה-SQL למטה:

SQL

 

select
            item0_.id as id1_0_,
            item0_.jsonb_content as jsonb_co2_0_ 
        from
            item item0_ 
        where
            jsonb_all_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true

"?|" מעטפת

הדוגמה לקוד למטה מדגימה איך ליצור שאילתה העוסקת ברשומות שבהן תכונת JSON המכילה מערך כולל לפחות אלמנט מחרוזת אחד שאנו מחפשים עליו.

Java

 

	private EntityManager entityManager;
    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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
        return entityManager.createQuery(query).getResultList();
    }

במקרה שהתגים יכילו שני אלמנטים אז Hibernate היה מייצר את ה-SQL למטה:

SQL

 

select
            item0_.id as id1_0_,
            item0_.jsonb_content as jsonb_co2_0_ 
        from
            item item0_ 
        where
            jsonb_any_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true

לדוגמאות נוספות על איך להשתמש באופרטורים מספריים, אנא בדוק את הדגמים אובייקט דאו ו-בדיקות דאו.

מסקנה

במקרים מסוימים, סוגי JSON ופונקציות של Postgres יכולות להוות אלטרנטיבות טובות לבסיסי נתונים NoSQL. זה יכול לחסוך לנו את ההחלטה להוסיף פתרונות NoSQL למסד הנתונים שלנו שיכולים גם להוסיף יותר מורכבות ועלויות נוספות.

Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-5