وظائف JSON في Postgres مع Hibernate 6

هذا هو استمرار للمقالة السابقة حيث تم وصف كيفية إضافة الدعم لدالات Postgres JSON واستخدام 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;
	//الحصول والتعيين
}

عندما يكون لدينا مثل هذا النموذج يمكننا على سبيل المثال الاستعلام بواسطة موصوف 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 واستخدام وظائف Postgres JSON التي تم تقديمها في المقالة السابقة. ولكن من المفيد أيضًا أن يكون لديك مثل هذه القدرة في استعلامات HQL أو عند استخدام المشغلات برمجياً. يكون هذا الأسلوب الثاني أكثر فائدة عندما يكون من المفترض أن تنفذ ميزة الاستعلام الديناميكي. على الرغم من أن تسلسل السلاسل النصية التي من المفترض أن تكون استعلام HQL قد يكون سهلًا ولكن الممارسة الأفضل هي استخدام المشغلات المُنفذة. هنا يأتي دور استخدام مكتبة posjsonhelper مفيدًا.

Posjsonhelper

المشروع موجود في مستودع مافن المركزي، لذا يمكنك بسهولة اضافته عن طريق إضافته كعنصر تبعية لمشروع مافن الخاص بك.

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 نوعًا مخصصًا (كما هو موضح أدناه)، ولكن يمكن أن تكون أيضًا نوع السلسلة.

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

-- عنصر بدون أي خصائص، مجرد جسون فارغ
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

-- قيم عددية
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}');

-- قيم مزدوجة
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}');

-- قيم تعدادية
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();
    }

سيولد Hibernate الشفرة ال 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 عندما يوفر Hibernate بعض الدعم لسمات JSON المساعدة للاستعلام

إلى جانب تلك العوامل التي تدعم أنواع المصفوفة المذكورة أعلاه، تحتوي المكتبة على عاملين مفيدين إضافيين. <كود>jsonb_extract_path و <كود>jsonb_extract_path_text هما غلافان لعوامل <كود>#> و <كود>#>>. يدعم Hibernate العامل <كود>->>. لرؤية الفرق بين تلك العوامل، يرجى مراجعة وثائق Postgres المرتبطة سابقا.

ومع ذلك، كما قرأت في بداية المقال، فإن دعم الاستعلام الأصلي للخصائص JSON مسموح فقط عندما يكون لفئة JSON خصائص بأنواع بسيطة. والأهم من ذلك، لا يمكنك الاستعلام عن سمة إذا لم تكن معرفة في النوع JSON. قد يكون ذلك مشكلة إذا افترضت أن بنية JSON الخاصة بك يمكن أن تكون أكثر ديناميكية ويمتلك بنية مرنة لا تحددها أي مخطط. 

باستخدام عامل التشغيل posjsonhelper، لا توجد مشكلة هذه. يمكنك الاستعلام عن أي سمة تريدها. لا يجب أن تكون معرفة كخاصية في نوع JSON. علاوة على ذلك، لا يجب أن تكون الخاصية في كياننا الذي يخزن عمود JSON كائنًا معقدًا مثل JsonbConent في أمثلتنا. يمكن أن تكون سلسلة بسيطة في Java.

الخاتمة

كما ذكر في المقال السابق، في بعض الحالات، يمكن أن تكون أنواع ووظائف JSON في Postgres بديلاً جيداً لـ قواعد البيانات NoSQL. يمكن أن يوفر لنا من اتخاذ قرار إضافة حلول NoSQL إلى مجموعة تكنولوجياتنا التي قد تضيف مزيدًا من التعقيد والتكاليف الإضافية.

هذا يمنحنا أيضًا المرونة عندما نحتاج إلى تخزين بيانات غير منظمة في قاعدة بياناتنا العلائقية والقدرة على الاستعلام في تلك البنى.

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