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

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

תמיכה מקורית

ל-Hibernate 6 כבר יש תמיכה טובה בתחביר של שאילתות לפי תכונות JSON כפי שמוצג בדוגמה הבאה:

יש לנו את המחלקה הגופנית הרגילה שיש לה תכונה אחת של JSON:

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;


@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 נראה כך:

Java

 

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.

Java

 

    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 שלנו אינו זקוק לתכונות עם סוג מורכב, אזי התמיכה המקורית מספיקה. 

נא לבדוק את הקישורים להלן לקבלת מידע נוסף:

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

Posjsonhelper

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

XML

 

<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.

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

הפתרון החלופי הוא להשתמש ברכיב com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher במהלך ההתחלה של היישום, כפי שמוצג בדוגמה להלן עם השימוש במסגרת ה-Spring.

Java

 

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".

דוגמה למודל

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

Java

 

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 (כפי שמוצג להלן), אך ייתכן שזה יהיה גם סוג מחרוזת.

Java

 

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 עבור השולחן:

SQL

 

create table item (
        id bigint 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"]}');

-- פריט ללא תכונות, רק 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 ובניית קריטריונים:

Java

 

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 כדלקמן:

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 מותאמות אישית המאגרות את המפעילים הללו. עבור ההגדרה הברירת מחדל, הפונקציות הללו יש את היישום להלן.

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

Java

 

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 להלן:

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

Java

 

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 להלן:

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