Base de données Postgres prend en charge quelques types JSON et opérations spéciales pour ces types.
Dans certains cas, ces opérations pourraient constituer une bonne alternative aux bases de données de documents comme MongoDB ou autres bases de données NoSQL. Bien sûr, les bases de données comme MongoDB pourraient avoir de meilleures procédures de réplication, mais ce sujet est en dehors du cadre de cet article.
Dans cet article, nous nous concentrerons sur la manière d’utiliser les opérations JSON dans les projets qui utilisent le framework Hibernate avec la version 5.
Exemple de Modèle
Notre modèle ressemble à l’exemple ci-dessous :
@Entity
@Table(name = "item")
public class Item {
@Id
private Long id;
@Column(name = "jsonb_content", columnDefinition = "jsonb")
private String jsonbContent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getJsonbContent() {
return jsonbContent;
}
public void setJsonbContent(String jsonbContent) {
this.jsonbContent = jsonbContent;
}
}
Important ! : Nous pourrions utiliser un type JSON spécifique pour la propriété jsonbContent
, mais dans la version 5 de Hibernate, cela ne donnerait aucun avantage du point de vue des opérations.
Opération DDL :
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
Pour des raisons de présentation, supposons que notre base de données contienne de telles donné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 aucune propriété, 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"]}}');
Approche de requête native
Dans Hibernate 5, nous pouvons utiliser une approche native où nous exécutons une commande SQL directe.
Important! :S’il te plaît, pour des raisons de présentation, omettre le fait que le code ci-dessous permet l’injection SQL pour l’expression de l’opérateur LIKE
. Bien sûr, pour une telle action, nous devrions utiliser des paramètres et PreparedStatement
.
private EntityManager entityManager;
public List<Item> findAllByStringValueAndLikeOperatorWithNativeQuery(String expression) {
return entityManager.createNativeQuery("SELECT * FROM item i WHERE i.jsonb_content#>>'{string_value}' LIKE '" + expression + "'", Item.class).getResultList();
}
Dans l’exemple ci-dessus, il y a l’utilisation de l’opérateur #>>
qui extrait l’objet JSON sub-objet à un chemin spécifié en tant que texte (s’il vous plaît consulter la documentation Postgres pour plus de détails).
Dans la plupart des cas, une telle requête (bien sûr, avec une valeur échappée) suffit. Cependant, si nous devons mettre en œuvre la création d’une sorte de requête dynamique basée sur les paramètres passés dans notre API, il serait préférable d’utiliser une sorte de constructeur de critères.
Posjsonhelper
Hibernate 5 ne prend pas en charge par défaut les fonctions Postgres JSON. Heureusement, vous pouvez l’implémenter vous-même ou utiliser la bibliothèque posjsonhelper qui est un projet open-source.
Le projet est présent dans le référentiel central Maven, vous pouvez donc l’ajouter facilement en l’ajoutant comme dépendance à votre projet Maven.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate5</artifactId>
<version>0.1.0</version>
</dependency>
Pour utiliser la bibliothèque posjsonhelper
dans votre projet, vous devez utiliser le dialecte Postgres mis en œuvre dans le projet. Par exemple :
com.github.starnowski.posjsonhelper.hibernate5.dialects.PostgreSQL95DialectWrapper ...
Si votre projet dispose déjà d’une classe de dialecte personnalisée, il est également possible d’utiliser :
com.github.starnowski.posjsonhelper.hibernate5.PostgreSQLDialectEnricher;
Utilisation des Composants de Critères
L’exemple ci-dessous présente un comportement similaire à l’exemple précédent qui utilisait une requête native. Cependant, dans ce cas, nous allons utiliser un constructeur de critères.
private EntityManager entityManager;
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((CriteriaBuilderImpl) cb, singletonList("string_value"), root.get("jsonbContent")), expression));
return entityManager.createQuery(query).getResultList();
}
Hibernate va générer le code SQL comme suit :
select
item0_.id as id1_0_,
item0_.jsonb_content as jsonb_co2_0_
from
item item0_
where
jsonb_extract_path_text(item0_.jsonb_content,?) like ?
La fonction jsonb_extract_path_text
est une fonction Postgres é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 Postgres comme :
?&
– Vérifie si tous les chaînes de caractères dans le tableau de texte existent comme 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, vous pouvez vérifier si elle contient tous les éléments que vous recherchez.?|
– Vérifie si l’une des chaînes de caractères dans le tableau de texte existe comme clé de niveau supérieur ou élément de tableau. Donc, généralement, si nous avons une propriété JSON qui contient un tableau, vous pouvez vérifier si elle contient au moins un des éléments que vous recherchez.
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. La bibliothèque Posjsonhelper
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 DDL requis, 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 recherchons.
private EntityManager entityManager;
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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
Dans le cas où les balises contiendraient deux éléments, Hibernate générerait le SQL ci-dessous:
select
item0_.id as id1_0_,
item0_.jsonb_content as jsonb_co2_0_
from
item item0_
where
jsonb_all_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true
« ?| » 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 au moins un élément de chaîne que nous recherchons.
private EntityManager entityManager;
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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
Dans le cas où les balises contiendraient deux éléments, Hibernate générerait le SQL ci-dessous :
select
item0_.id as id1_0_,
item0_.jsonb_content as jsonb_co2_0_
from
item item0_
where
jsonb_any_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true
Pour plus d’exemples sur l’utilisation des opérateurs numériques, veuillez consulter la démo objet dao et tests dao.
Conclusion
Dans certains cas, les types et fonctions JSON de Postgres peuvent être de bonnes alternatives aux 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 des coûts supplémentaires.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-5