Ceci est une suite de l’article précédent où il a été décrit comment ajouter un support pour les fonctions JSON de Postgres et utiliser Hibernate 5. Dans cet article, nous allons nous concentrer sur la façon d’utiliser les opérations JSON dans les projets qui utilisent le framework Hibernate avec la version 6.
Support natif
Hibernate 6 dispose déjà d’un bon support pour la recherche par attributs JSON comme le montre l’exemple ci-dessous.
Nous avons notre classe d’entité normale qui possède une propriété 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;
}
}
Le type JsonbContent
ressemble à ceci :
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 et Setters
}
Lorsque nous avons un tel modèle, nous pouvons par exemple interroger par l’attribut 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();
}
Important! – Actuellement, il semble y avoir une limitation avec le support de la recherche par attributs, qui est que nous ne pouvons pas interroger par des types complexes comme les tableaux. Comme vous pouvez le voir, le type JsonbContent
a l’annotation Embeddable
, ce qui signifie que si vous essayez d’ajouter une propriété qui est une liste, nous pourrions voir une exception avec le message suivant : Le type qui est censé être sérialisé en JSON ne peut pas avoir de types complexes comme ses propriétés : Les composants agrégés ne peuvent contenir actuellement que des valeurs simples de base et des composants de valeurs de base simples.
Dans le cas où notre type JSON n’a pas besoin d’avoir des propriétés de type complexe, alors un support natif suffit.
Veuillez consulter les liens ci-dessous pour plus d’informations:
- Stack Overflow : Navigation JSON avec Hibernate 6.2
- Hibernate ORM 6.2 – Mappages d’agrégats composites
- GitHub : hibernate6-tests-native-support-1
Cependant, il est parfois utile d’avoir la possibilité de faire des requêtes par attributs d’array. Bien sûr, nous pouvons utiliser des requêtes SQL natives dans Hibernate et utiliser les fonctions JSON de Postgres qui ont été présentées dans l’article précédent. Mais il serait également utile d’avoir cette possibilité dans les requêtes HQL ou lors de l’utilisation de prédicats programmatiquement. Cette deuxième approche est encore plus utile lorsque vous êtes censé mettre en œuvre la fonctionnalité d’une requête dynamique. Bien que concaténer dynamiquement une chaîne censée être une requête HQL puisse être facile, une meilleure pratique serait d’utiliser des prédicats mis en œuvre. C’est là que l’utilisation de la bibliothèque posjsonhelper devient pratique.
Posjsonhelper
Le projet existe dans le référentiel central Maven, vous pouvez donc l’ajouter facilement en l’ajoutant en tant que dépendance à votre projet Maven.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate6</artifactId>
<version>0.2.1</version>
</dependency>
Inscription de FunctionContributor
Pour utiliser la bibliothèque, nous devons attacher le composant FunctionContributor
. Nous pouvons le faire de deux manières. La première et la plus recommandée est de créer un fichier portant le nom org.hibernate.boot.model.FunctionContributor dans le répertoire resources/META-INF/services.
En tant que contenu du fichier, il suffit de placer l’implémentation posjsonhelper
du type org.hibernate.boot.model.FunctionContributor
.
com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor
La solution alternative consiste à utiliser le composant com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher
lors du démarrage de l’application, comme dans l’exemple ci-dessous avec l’utilisation du 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());
}
}
Pour plus de détails, veuillez consulter « Comment attacher FunctionContributor. »
Modèle d’exemple
Notre modèle ressemble à l’exemple ci-dessous:
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;
}
}
Important!: Dans cet exemple, la propriété JsonbConent
est un type personnalisé (comme ci-dessous), mais elle pourrait également être du type 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;
// Setters et Getters
}
Opérations DDL pour la table:
create table item (
id bigint not null,
jsonb_content jsonb,
primary key (id)
)
Pour des raisons de présentation, supposons que notre base de données contienne de telles entrées :
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"]}');
-- élément sans propriétés, juste un json vide
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');
-- valeurs 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}');
-- valeurs 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}');
-- valeurs 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"}');
-- valeurs string
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"}');
-- éléments internes
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"]}}');
Utilisation des Composants de Critères
Voici un exemple de la même requête présentée au début, mais créée avec des composants SQM et un constructeur de critères:
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 va générer le code SQL comme suit:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''
Le jsonb_extract_path_text
est une fonction Postgres qui est équivalente à l’opérateur #>>
(veuillez consulter la documentation Postgres liée précédemment pour plus de détails).
Opérations sur les Tableaux
La bibliothèque prend en charge quelques opérateurs de fonctions JSON de Postgres, tels que:
?&
– Cela vérifie si tous les chaînes de caractères dans le tableau de texte existent en tant que clés de niveau supérieur ou éléments de tableau. Donc, généralement, si nous avons une propriété JSON qui contient un tableau, alors vous pouvez vérifier s’il contient tous les éléments que vous recherchez.?|
– Cela vérifie si l’une des chaînes de caractères dans le tableau de texte existe en tant que clés de niveau supérieur ou éléments de tableau. Donc, généralement, si nous avons une propriété JSON qui contient un tableau, alors vous pouvez vérifier s’il contient au moins un des éléments que vous recherchez.
En plus d’exécuter des requêtes SQL natives, Hibernate 6 ne prend pas en charge les opérations ci-dessus.
Modifications DDL requises
L’opérateur ci-dessus ne peut pas être utilisé dans HQL en raison de caractères spéciaux. C’est pourquoi nous devons les envelopper, par exemple, dans une fonction SQL personnalisée. Posjsonhelper
la bibliothèque nécessite deux fonctions SQL personnalisées qui envelopperont ces opérateurs. Pour la configuration par défaut, ces fonctions auront l’implémentation ci-dessous.
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;
Pour plus d’informations sur la façon de personnaliser ou d’ajouter par programmation les modifications DDL requises, veuillez consulter la section « Appliquer les modifications DDL« .
« ?& » Wrapper
L’exemple de code ci-dessous illustre comment créer une requête qui examine les enregistrements pour lesquels la propriété JSON contenant un tableau a tous les éléments de chaîne que nous utilisons pour rechercher.
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();
}
Dans le cas où les balises contiennent deux éléments, Hibernate générerait le SQL ci-dessous:
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
Le code de l’exemple ci-dessous illustre comment créer une requête qui examine les enregistrements pour lesquels la propriété JSON contient un tableau et a au moins un élément de chaîne que nous utilisons pour rechercher.
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();
}
Dans le cas où les balises contiennent deux éléments, Hibernate générerait le SQL ci-dessous:
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[?,?])
Pour plus d’exemples sur l’utilisation des opérateurs numériques, veuillez consulter la démo objet dao et tests dao.
Pourquoi utiliser la bibliothèque posjsonhelper alors que Hibernate offre un soutien pour les attributs JSON
En plus des deux opérateurs qui prennent en charge les types d’array mentionnés ci-dessus, la bibliothèque possède deux opérateurs supplémentaires utiles. Le jsonb_extract_path
et jsonb_extract_path_text
sont des enveloppes pour les opérateurs #>
et #>>
. Hibernate prend en charge l’opérateur ->>
. Pour voir la différence entre ces opérateurs, veuillez consulter la documentation de Postgres liée précédemment.
Cependant, comme vous l’avez lu au début de l’article, le support des requêtes natives pour les attributs JSON n’est autorisé que lorsque la classe JSON possède des propriétés de types simples. Et surtout, vous ne pouvez pas interroger par attribut s’il n’est pas mappé à la propriété dans le type JSON. Cela pourrait poser problème si vous supposez que votre structure JSON peut être plus dynamique et avoir une structure élastique non définie par aucun schéma.
Avec l’opérateur posjsonhelper
, vous n’avez pas ce problème. Vous pouvez interroger par n’importe quel attribut que vous souhaitez. Il n’a pas besoin d’être défini comme propriété dans le type JSON. De plus, la propriété de notre entité qui stocke la colonne JSON n’a pas besoin d’être un objet complexe comme JsonbConent
dans nos exemples. Elle peut être une simple chaîne en Java.
Conclusion
Comme mentionné dans l’article précédent, dans certains cas, les types et fonctions JSON de Postgres peuvent être de bonnes alternatives pour bases de données NoSQL. Cela pourrait nous épargner la décision d’ajouter des solutions NoSQL à notre stack technologique, ce qui pourrait également ajouter plus de complexité et de coûts supplémentaires.
Cela nous donne également la flexibilité dont nous avons besoin pour stocker des données non structurées dans notre base relationnelle et la possibilité d’interroger dans ces structures.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6