Questo è un seguito dell’articolo precedente dove è stato descritto come aggiungere supporto per le funzioni JSON di Postgres e utilizzare Hibernate 5. In questo articolo, ci concentreremo su come utilizzare le operazioni JSON in progetti che utilizzano il framework Hibernate con la versione 6.
Supporto Nativo
Hibernate 6 dispone già di un buon supporto per le query tramite attributi JSON come mostra l’esempio seguente.
Abbiamo la nostra normale classe entità che ha un proprietà JSON:
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;
}
}
Il tipo JsonbContent
è simile al seguente:
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 e Setters
}
Quando abbiamo un modello del genere, possiamo ad esempio effettuare una query sull’attributo string_value
.
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();
}
Importante! – Attualmente, sembra esserci una limitazione nel supporto delle query per attributi, vale a dire che non possiamo effettuare query su tipi complessi come array. Come puoi vedere, il tipo JsonbContent
ha l’annotazione Embeddable
, il che significa che se provi ad aggiungere una proprietà che è una lista potremmo vedere un’eccezione con il seguente messaggio: Il tipo che dovrebbe essere serializzato come JSON non può avere tipi complessi come sue proprietà: I componenti aggregati attualmente possono contenere solo valori semplici di base e componenti di valori semplici di base.
Nel caso in cui il nostro tipo JSON non necessiti di proprietà con un tipo complesso, allora il supporto nativo è sufficiente.
Per ulteriori informazioni, consultare i seguenti collegamenti:
- Stack Overflow: Hibernate 6.2 e navigazione JSON
- Hibernate ORM 6.2 – Mappature aggregate composite
- GitHub: hibernate6-tests-native-support-1
Tuttavia, a volte è utile avere la possibilità di effettuare query per attributi di array. Naturalmente, possiamo utilizzare query SQL native in Hibernate e utilizzare le funzioni JSON di Postgres, presentate nell’articolo precedente. Ma sarebbe anche utile avere questa possibilità nelle query HQL o quando si utilizzano predicati a livello di programmazione. Questo secondo approccio è ancora più utile quando si deve implementare la funzionalità di una query dinamica. Sebbene concatenare dinamicamente una stringa che dovrebbe essere una query HQL possa essere facile, sarebbe una migliore pratica utilizzare predicati implementati. È qui che l’uso della libreria posjsonhelper diventa utile.
Posjsonhelper
Il progetto è presente nel repository centrale Maven, quindi puoi aggiungerlo facilmente aggiungendolo come dipendenza al tuo progetto Maven.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate6</artifactId>
<version>0.2.1</version>
</dependency>
Registrare FunctionContributor
Per utilizzare la libreria, dobbiamo allegare il componente FunctionContributor
. Possiamo farlo in due modi. Il primo e più raccomandato è creare un file con il nome org.hibernate.boot.model.FunctionContributor nella directory resources/META-INF/services.
Come contenuto del file, basta inserire l’implementazione posjsonhelper
del tipo org.hibernate.boot.model.FunctionContributor
.
com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor
La soluzione alternativa è utilizzare il componente com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher
durante l’avvio dell’applicazione, come nell’esempio seguente con l’utilizzo del Spring Framework.
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());
}
}
Per ulteriori dettagli, si prega di consultare “Come allegare FunctionContributor.”
Esempio di Modello
Il nostro modello è simile all’esempio seguente:
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;
}
}
Importante!: In questo esempio, la proprietà JsonbConent
è un tipo personalizzato (come di seguito), ma potrebbe anche essere il tipo String.
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;
// Setter e Getter
}
Operazioni DDL per la tabella:
create table item (
id bigint not null,
jsonb_content jsonb,
primary key (id)
)
A scopo di presentazione, supponiamo che il nostro database contenga tali record:
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"]}');
-- elemento senza proprietà, solo un json vuoto
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');
-- valori 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}');
-- valori 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}');
-- valori 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"}');
-- valori stringa
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"}');
-- elementi interni
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"]}}');
Utilizzo dei Componenti dei Criteri
Di seguito è riportato un esempio dello stesso query presentato all’inizio, ma creato con componenti SQM e builder di criteri:
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 genererà il codice SQL come segue:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''
Il jsonb_extract_path_text
è una funzione Postgres che equivale all’operatore #>>
(consulta la documentazione di Postgres linkata in precedenza per maggiori dettagli).
Operazioni su Array
La libreria supporta alcuni operatori funzionali JSON di Postgres, come:
?&
– Questo controlla se tutte le stringhe nell’array di testo esistono come chiavi di primo livello o elementi dell’array. Quindi generalmente, se abbiamo una proprietà JSON che contiene un array, allora puoi verificare se contiene tutti gli elementi che stai cercando.?|
– Questo controlla se qualsiasi delle stringhe nell’array di testo esiste come chiavi di primo livello o elementi dell’array. Quindi generalmente, se abbiamo una proprietà JSON che contiene un array, allora puoi verificare se contiene almeno gli elementi che stai cercando.
Oltre a eseguire query SQL native, Hibernate 6 non supporta le operazioni sopra elencate.
Modifiche DDL richieste
L’operatore sopra non può essere utilizzato in HQL a causa di caratteri speciali. Ecco perché abbiamo bisogno di avvolgerli, ad esempio, in una funzione SQL personalizzata. Posjsonhelper
la libreria richiede due funzioni SQL personalizzate che avvolgono quegli operatori. Per la configurazione predefinita, queste funzioni avranno la seguente implementazione.
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;
Per ulteriori informazioni su come personalizzare o aggiungere in modo programmatico le DDL richieste, consultare la sezione “Applica modifiche DDL.”
“?&” Wrapper
Il codice di esempio riportato di seguito illustra come creare una query che esamina i record per i quali la proprietà JSON che contiene un array ha tutti gli elementi di stringa che stiamo utilizzando per la ricerca.
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();
}
Nel caso in cui i tag contengano due elementi, Hibernate genererebbe il seguente 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
Il codice nell’esempio seguente illustra come creare una query che esamina i record per i quali la proprietà JSON contiene un array e ha almeno un elemento di stringa che stiamo utilizzando per la ricerca.
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();
}
Nel caso in cui i tag contengano due elementi, Hibernate genererebbe il seguente 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[?,?])
Per ulteriori esempi di come utilizzare gli operatori numerici, si prega di consultare la demo oggetto dao e test dao.
Perché utilizzare la libreria posjsonhelper quando Hibernate offre già un supporto per le query su attributi JSON
Oltre a quei due operatori che supportano i tipi di array menzionati sopra, la libreria offre due operatori aggiuntivi utili. Il jsonb_extract_path
e il jsonb_extract_path_text
sono wrapper per gli operatori #>
e #>>
. Hibernate supporta l’operatore ->>
. Per vedere la differenza tra questi operatori, si prega di consultare la documentazione di Postgres collegata in precedenza.
Tuttavia, come hai letto all’inizio dell’articolo, il supporto nativo per le query su attributi JSON è consentito solo quando la classe JSON ha proprietà con tipi semplici. E, soprattutto, non è possibile effettuare query per attributo se non è mappato alla proprietà nel tipo JSON. Questo potrebbe essere un problema se si presume che la struttura JSON possa essere più dinamica e avere una struttura elastica non definita da alcun schema.
Con l’operatore posjsonhelper
, non hai questo problema. Puoi effettuare query su qualsiasi attributo che desideri. Non deve essere definito come proprietà nel tipo JSON. Inoltre, la proprietà nella nostra entità che memorizza la colonna JSON non deve essere un oggetto complesso come JsonbContent
nei nostri esempi. Può essere una semplice stringa in Java.
Conclusione
Come menzionato nell’articolo precedente, in alcuni casi, i tipi e le funzioni JSON di Postgres possono essere buone alternative per database NoSQL. Questo potrebbe evitare la decisione di aggiungere soluzioni NoSQL alla nostra pila tecnologica, che potrebbe anche aggiungere più complessità e costi aggiuntivi.
Ciò ci offre anche flessibilità quando abbiamo bisogno di memorizzare dati non strutturati nel nostro database relazionale e la possibilità di eseguire query in quelle strutture.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6