Postgres JSON-Funktionen mit Hibernate 6

Dies ist eine Fortsetzung des vorherigen Artikels, in dem beschrieben wurde, wie Unterstützung für die Postgres JSON-Funktionen hinzuzufügen und Hibernate 5 zu verwenden. In diesem Artikel konzentrieren wir uns darauf, wie JSON-Operationen in Projekten verwendet werden, die auf dem Hibernate-Framework mit Version 6 aufbauen. 

Native Support

Hibernate 6 verfügt bereits über eine gute Unterstützung für Abfragen nach JSON-Attributen, wie das folgende Beispiel zeigt.

Wir haben unsere normale Entitätsklasse, die ein JSON-Attribut hat:

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;
    }
}

Der JsonbContent-Typ sieht wie folgt aus:

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;
	//Getter und Setter
}

Wenn wir ein solches Modell haben, können wir zum Beispiel nach dem string_value-Attribut abfragen.

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();
    }

Wichtig! – Derzeit scheint es Einschränkungen bei der Unterstützung von Abfragen nach Attributen zu geben, die darin bestehen, dass wir nicht nach komplexen Typen wie Arrays abfragen können. Wie zu sehen ist, hat der JsonbContent-Typ die Embeddable-Annotation, was bedeutet, dass wenn Sie versuchen, ein Attribut hinzuzufügen, das eine Liste ist, wir möglicherweise eine Ausnahme mit der folgenden Meldung erhalten: Der als JSON serialisierte Typ darf keine komplexen Typen als seine Eigenschaften haben: Aggregate Komponenten dürfen derzeit nur einfache Grundwerte und Komponenten einfacher Grundwerte enthalten.

Im Fall, in dem unser JSON-Typ keine Eigenschaften mit einem komplexen Typ benötigt, reicht die native Unterstützung aus. 

Bitte überprüfen Sie die folgenden Links für weitere Informationen:

Manchmal ist es jedoch ratsam, die Möglichkeit zu haben, nach Array-Attributen zu suchen. Natürlich können wir in Hibernate native SQL-Abfragen verwenden und die in dem vorherigen Artikel vorgestellten Postgres-JSON-Funktionen nutzen. Es wäre jedoch auch von Vorteil, solche Möglichkeiten in HQL-Abfragen oder bei der Verwendung von programmatischen Prädikaten zu haben. Der zweite Ansatz ist besonders nützlich, wenn Sie die Funktionalität einer dynamischen Abfrage implementieren sollen. Obwohl das dynamische Verketten einer Zeichenfolge, die eine HQL-Abfrage sein soll, einfach sein mag, wäre es eine bessere Praxis, implementierte Prädikate zu verwenden. Hierbei wird die Verwendung der posjsonhelper-Bibliothek nützlich.

Posjsonhelper

Das Projekt ist im Maven Zentralen Repository verfügbar, sodass Sie es leicht hinzufügen können, indem Sie es als Abhängigkeit zu Ihrem Maven-Projekt hinzufügen.

XML

 

<dependency>
            <groupId>com.github.starnowski.posjsonhelper</groupId>
            <artifactId>hibernate6</artifactId>
            <version>0.2.1</version>
</dependency>

Register FunctionContributor

Um die Bibliothek zu verwenden, müssen wir das FunctionContributor Komponente anhängen. Das können wir auf zwei Arten tun. Die erste und am meisten empfohlene Methode ist, eine Datei mit dem Namen org.hibernate.boot.model.FunctionContributor im Verzeichnis resources/META-INF/services zu erstellen. 

Als Inhalt der Datei setzen Sie einfach die posjsonhelper Implementierung des Typs org.hibernate.boot.model.FunctionContributor.

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

Die alternative Lösung besteht darin, während des Anwendungsstart-ups das com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher Komponente zu verwenden, wie im folgenden Beispiel mit der Verwendung des Spring Frameworks.

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());
    }
}

Weitere Details finden Sie unter „Wie man FunctionContributor anhängt.“

Beispiel Modell

Unser Modell sieht wie das folgende Beispiel aus:

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;
    }
}

Wichtig!: In diesem Beispiel ist die JsonbConent Eigenschaft ein benutzerdefinierter Typ (wie unten beschrieben), könnte aber auch der Typ String sein.

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;

// Setters und Getter
}

DDL-Operationen für die Tabelle:

SQL

 

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

Zur Veranschaulichung nehmen wir an, dass unsere Datenbank solche Einträge enthält:  

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

-- Element ohne Eigenschaften, nur ein leeres JSON
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

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

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

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

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

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

Verwendung von Kriterienkomponenten

Im Folgenden ein Beispiel für dieselbe Abfrage, die am Anfang präsentiert wurde, aber mit SQM-Komponenten und Kriterien-Builder erstellt:

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 generiert den SQL-Code wie folgt:

SQL

 

select
        i1_0.id,
        i1_0.jsonb_content 
    from
        item i1_0 
    where
        jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''

Die Funktion jsonb_extract_path_text ist eine Postgres-Funktion, die dem Operator #>> entspricht (für weitere Details siehe die zuvor verlinkte Postgres-Dokumentation).

Operationen auf Arrays

Die Bibliothek unterstützt einige Postgres JSON-Funktionsoperatoren, wie:

  • ?& – Dieser prüft, ob alle Zeichenfolgen im Textarray als oberste Schlüssel oder Arrayelemente existieren. Im Allgemeinen können Sie also überprüfen, ob ein JSON-Attribut, das ein Array enthält, alle von Ihnen gesuchten Elemente enthält.
  • ?| – Dieser prüft, ob eine der Zeichenfolgen im Textarray als oberste Schlüssel oder Arrayelemente existiert. Im Allgemeinen können Sie also überprüfen, ob ein JSON-Attribut, das ein Array enthält, mindestens eines der von Ihnen gesuchten Elemente enthält.

Neben der Ausführung von nativen SQL-Abfragen unterstützt Hibernate 6 die oben genannten Operationen nicht.

Erforderliche DDL-Änderungen

Der obige Operator kann in HQL aufgrund besonderer Zeichen nicht verwendet werden. Deshalb müssen wir sie zum Beispiel in einer benutzerdefinierten SQL-Funktion einkapseln. Die Posjsonhelper-Bibliothek benötigt zwei benutzerdefinierte SQL-Funktionen, die diese Operatoren einkapseln. Für die Standardeinstellung haben diese Funktionen die untenstehende Implementierung.

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;

Weitere Informationen zum Anpassen oder Programmatisch hinzufügen erforderlicher DDL finden Sie im Abschnitt „DDL-Änderungen anwenden.“

„?&“ Wrapper

Der folgende Codebeispiel veranschaulicht, wie eine Abfrage erstellt wird, die sich anhand von Datensätzen orientiert, bei denen die JSON-Eigenschaft, die ein Array enthält, alle Zeichenfolgenelemente enthält, nach denen wir suchen.

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();
    }

Falls die Tags zwei Elemente enthalten, würde Hibernate die folgende SQL generieren:

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[?,?])

„?|“ Wrapper

Der Code im Beispiel unten zeigt, wie eine Abfrage erstellt wird, die sich anhand von Datensätzen orientiert, bei denen die JSON-Eigenschaft ein Array enthält und mindestens ein Zeichenfolgenelement enthält, nach dem wir suchen.

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();
    }

Falls die Tags zwei Elemente enthalten, würde Hibernate die folgende SQL generieren:

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[?,?])

Für weitere Beispiele zur Verwendung numerischer Operatoren lesen Sie bitte die Demo dao object und dao tests.

Warum die posjsonhelper-Bibliothek verwenden, wenn Hibernate bereits Unterstützung für JSON-Attributabfragen bietet

Neben den zwei Operatoren, die die oben genannten Array-Typen unterstützen, verfügt die Bibliothek über zwei zusätzliche nützliche Operatoren. Die jsonb_extract_path und jsonb_extract_path_text sind Wrapper für die Operatoren #> und #>>. Hibernate unterstützt den ->> Operator. Um den Unterschied zwischen diesen Operatoren zu sehen, lesen Sie bitte die zuvor verlinkte Postgres-Dokumentation.

Allerdings, wie Sie am Anfang des Artikels gelesen haben, ist die native Abfrageunterstützung für JSON-Attribute nur zulässig, wenn die JSON-Klasse Eigenschaften mit einfachen Typen hat. Und noch wichtiger ist, dass man nicht nach Attributen abfragen kann, wenn sie nicht auf eine Eigenschaft im JSON-Typ abgebildet sind. Das könnte ein Problem sein, wenn man davon ausgeht, dass die JSON-Struktur dynamischer sein kann und eine elastische Struktur hat, die nicht durch ein Schema definiert ist. 

Mit dem posjsonhelper Operator haben Sie dieses Problem nicht. Sie können nach beliebigen Attributen abfragen, die Sie wünschen. Es muss nicht als Eigenschaft im JSON-Typ definiert sein. Darüber hinaus muss die Eigenschaft in unserer Entität, die die JSON-Spalte speichert, nicht ein komplexes Objekt wie JsonbContent in unseren Beispielen sein. Es kann ein einfacher String in Java sein.

Schlussfolgerung

Wie bereits in dem vorherigen Artikel erwähnt, können in einigen Fällen Postgres JSON-Typen und Funktionen gute Alternativen für NoSQL-Datenbanken sein. Dies könnte uns davor bewahren, NoSQL-Lösungen in unser Technologiestapel aufzunehmen, was auch mehr Komplexität und zusätzliche Kosten verursachen könnte.

Das gibt uns auch Flexibilität, wenn wir unstrukturierte Daten in unserer relationalen Basis speichern müssen und die Möglichkeit, in diesen Strukturen zu suchen.

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