Postgres JSON-functies met Hibernate 6

Dit is een voortzetting van het vorige artikel waarin werd beschreven hoe ondersteuning toevoegt voor de Postgres JSON functies en het gebruik van Hibernate 5. In dit artikel richten we ons op hoe JSON-operaties te gebruiken in projecten die het Hibernate-framework gebruiken met versie 6. 

Native Support

Hibernate 6 heeft al enige goede ondersteuning voor query’s op JSON-attributen zoals het onderstaande voorbeeld laat zien.

We hebben onze normale entiteitklasse die één JSON-eigenschap heeft:

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

Het type JsonbContent ziet er als volgt uit:

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 en Setters
}

Wanneer we zo’n model hebben, kunnen we bijvoorbeeld opvragen door het string_value attribuut.

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

Belangrijk! – Momenteel lijkt er een beperking te zijn met de ondersteuning voor query’s op attributen, namelijk dat we geen query’s kunnen uitvoeren op complexe typen zoals arrays. Zoals te zien is, heeft het type JsonbContent de Embeddable annotatie, wat betekent dat als je probeert een eigenschap toe te voegen die een lijst is, we misschien een uitzondering zien met de volgende boodschap: Het type dat als JSON moet worden gerenderd, mag geen complexe typen als eigenschappen hebben: Samengestelde componenten mogen momenteel alleen eenvoudige basiswaarden en componenten van eenvoudige basiswaarden bevatten. 

In het geval dat onze JSON-type geen eigenschappen met een complex type nodig heeft, is standaard ondersteuning voldoende. 

Raadpleeg de onderstaande links voor meer informatie:

Soms is het echter zinvol om de mogelijkheid te hebben om op tevragen naar array-eigenschappen. Uiteraard kunnen we in Hibernate native SQL-query’s gebruiken en Postgres JSON-functies gebruiken die in het vorige artikel zijn besproken. Maar het zou ook handig zijn om zulke mogelijkheid in HQL-query’s of bij het gebruik van programmatische predikaten te hebben. Dit tweede aanpak is zelfs nuttiger wanneer u de functionaliteit van een dynamische query moet implementeren. Hoewel het dynamisch samenvoegen van een string die een HQL-query moet zijn misschien eenvoudig is, is het beter om geïmplementeerde predikaten te gebruiken. Dit is waar het gebruik van de posjsonhelper bibliotheek handig wordt.

Posjsonhelper

Het project bevindt zich in de Maven centrale opslagplaats, zodat u het gemakkelijk kunt toevoegen door het als afhankelijkheid aan uw Maven-project toe te voegen.

XML

 

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

Registreer FunctionContributor

Om de bibliotheek te gebruiken, moeten we het FunctionContributor component koppelen. Dit kunnen we op twee manieren doen. De eerste en meest aanbevolen manier is het maken van een bestand met de naam org.hibernate.boot.model.FunctionContributor in de resources/META-INF/services directory. 

Als inhoud van het bestand, plaatst u gewoon de posjsonhelper implementatie van het org.hibernate.boot.model.FunctionContributor type.

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

De alternatieve oplossing is het gebruik van het com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher component tijdens de opstart van de applicatie, zoals in het onderstaande voorbeeld met behulp van de Spring Framework.

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

Voor meer details kijk dan naar “Hoe FunctionContributor koppelen.”

Voorbeeld Model

Ons model ziet eruit als het onderstaande voorbeeld:

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

Belangrijk!: In dit voorbeeld is de JsonbContent eigenschap een aangepaste type (zoals hieronder), maar het kan ook het String-type zijn.

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 en Getters
}

DDL-bewerkingen voor de tabel:

SQL

 

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

Voor presentatiedoeleinden, laten we aannemen dat onze database zulke records bevat:  

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

-- item zonder eigenschappen, gewoon een leeg json
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

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

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

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

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

Met behulp van Criteria Components

Hieronder is een voorbeeld van dezelfde query die eerder is gepresenteerd, maar gemaakt met SQM-componenten en criteria builder:

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 gaat de SQL-code genereren zoals hieronder:

SQL

 

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

De jsonb_extract_path_text is een Postgres-functie die equivalent is aan de #>> operator (zie de eerder genoemde Postgres-documentatie voor meer details).

Bewerkingen op Arrays

De bibliotheek ondersteunt een paar Postgres JSON-functie-operators, zoals:

  • ?& – Dit controleert of alle strings in de tekstarray bestaan als top-level sleutels of array-elementen. Dus over het algemeen, als we een JSON-eigenschap hebben die een array bevat, kun je controleren of deze alle elementen bevat die je zoekt.
  • ?| – Dit controleert of een van de strings in de tekstarray bestaat als top-level sleutel of array-element. Dus over het algemeen, als we een JSON-eigenschap hebben die een array bevat, kun je controleren of deze minstens een van de elementen bevat die je zoekt.

Naast het uitvoeren van native SQL-query’s, ondersteunt Hibernate 6 de bovenstaande bewerkingen niet.

Vereiste DDL-wijzigingen

De operator hierboven kan niet worden gebruikt in HQL vanwege speciale tekens. Daarom moeten we ze omsluiten, bijvoorbeeld in een aangepaste SQL-functie. De bibliotheek Posjsonhelper vereist twee aangepaste SQL-functies die deze operatoren omsluiten. Voor de standaardinstelling zullen deze functies de onderstaande implementatie hebben.

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;

Ga voor meer informatie over het aanpassen of programmatisch toevoegen van vereiste DDL naar de sectie “Toepassen van DDL-wijzigingen.”

“?&” Wrapper

Het onderstaande codevoorbeeld illustreert hoe u een query kunt maken die kijkt naar records waarvan de JSON-eigenschap die een array bevat, alle tekenreekselementen bevat die we gebruiken om te zoeken. 

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

Als de tags twee elementen bevatten, zou Hibernate de onderstaande SQL genereren:

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

Het codevoorbeeld hieronder illustreert hoe u een query kunt maken die kijkt naar records waarvan de JSON-eigenschap een array bevat en minstens één tekenreekselement bevat dat we gebruiken om te zoeken.

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

Als de tags twee elementen bevatten, zou Hibernate de onderstaande SQL genereren:

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

Voor meer voorbeelden van hoe u numerieke operatoren kunt gebruiken, bekijk de demo dao object en dao tests.

Waarom gebruik je de posjsonhelper-bibliotheek als Hibernate al enige ondersteuning biedt voor JSON-kenmerkenquery

Naast de twee operatoren die de arraytypen hierboven genoemd ondersteunen, heeft de bibliotheek twee extra nuttige operatoren. De jsonb_extract_path en jsonb_extract_path_text zijn wrappers voor de #> en #>> operatoren. Hibernate ondersteunt de ->> operator. Om het verschil tussen deze operatoren te zien, bekijk de Postgres-documentatie die eerder is gekoppeld.

Echter, zoals u aan het begin van het artikel las, is de ondersteuning voor native query’s voor JSON-attributen alleen toegestaan wanneer de JSON-klasse eigenschappen heeft met eenvoudige typen. En nog belangrijker, u kunt niet op eigenschap zoeken als deze niet is toegewezen aan een eigenschap in het JSON-type. Dat kan een probleem zijn als u ervan uitgaat dat uw JSON-structuur dynamischer kan zijn en een elastische structuur heeft die niet wordt gedefinieerd door een schema. 

Met de posjsonhelper operator heb je dit probleem niet. Je kunt zoeken op elk attribuut dat je wilt. Het hoeft niet gedefinieerd te zijn als een eigenschap in het JSON-type. Bovendien hoeft de eigenschap in onze entiteit die de JSON-kolom opslaat niet een complex object te zijn zoals JsonbConent in onze voorbeelden. Het kan een eenvoudige string zijn in Java.

Conclusie

Zoals eerder in het artikel werd genoemd, kunnen Postgres JSON-typen en -functies in sommige gevallen goede alternatieven zijn voor NoSQL databases. Dit kan ons ervan redden om NoSQL-oplossingen aan onze technologie-stack toe te voegen, wat ook meer complexiteit en extra kosten kan opleveren.

Dat geeft ons ook flexibiliteit wanneer we ongestructureerde gegevens in onze relationele database moeten opslaan en de mogelijkheid om in die structuren te zoeken.

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