Esta es una continuación del artículo anterior donde se describió cómo agregar soporte para las funciones JSON de Postgres y usar Hibernate 5. En este artículo, nos enfocaremos en cómo utilizar operaciones JSON en proyectos que utilizan el marco de Hibernate con la versión 6.
Soporte Nativo
Hibernate 6 ya cuenta con un buen soporte para consultas por atributos JSON como lo presenta el siguiente ejemplo.
Tenemos nuestra clase de entidad normal que tiene una propiedad 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;
}
}
El tipo JsonbContent
se ve como el siguiente:
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 y Setters
}
Cuando tenemos un modelo así, podemos, por ejemplo, consultar por el atributo 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! – Actualmente, parece haber alguna limitación con el soporte de consulta por atributos, que es que no podemos consultar por tipos complejos como arrays. Como puedes ver, el tipo JsonbContent
tiene la anotación Embeddable
, lo que significa que si intentas agregar alguna propiedad que sea una lista, podríamos ver una excepción con el siguiente mensaje: El tipo que se supone que debe serializarse como JSON no puede tener tipos complejos como sus propiedades: Los componentes agregados actualmente solo pueden contener valores básicos simples y componentes de valores básicos simples.
En el caso en que nuestro tipo JSON no necesite tener propiedades con un tipo complejo, entonces el soporte nativo es suficiente.
Por favor revise los siguientes enlaces para obtener más información:
- Stack Overflow: Navegación JSON en Hibernate 6.2
- Hibernate ORM 6.2 – Mapeos de agregados compuestos
- GitHub: hibernate6-tests-native-support-1
Sin embargo, a veces vale la pena tener la posibilidad de consultar por atributos de matriz. Por supuesto, podemos usar consultas SQL nativas en Hibernate y utilizar funciones JSON de Postgres que se presentaron en el artículo anterior. Pero también sería útil tener tal posibilidad en consultas HQL o al usar predicados de manera programática. Esta segunda aproximación es aún más útil cuando se supone que se debe implementar la funcionalidad de una consulta dinámica. Aunque concatenar dinámicamente una cadena que se supone que es una consulta HQL puede ser fácil, la mejor práctica sería usar predicados implementados. Aquí es donde el uso de la biblioteca posjsonhelper resulta útil.
Posjsonhelper
El proyecto está disponible en el repositorio central Maven, por lo que puedes agregarlo fácilmente añadiéndolo como una dependencia a tu proyecto Maven.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate6</artifactId>
<version>0.2.1</version>
</dependency>
Registro de FunctionContributor
Para utilizar la biblioteca, debemos adjuntar el componente FunctionContributor
. Podemos hacerlo de dos maneras. La primera y más recomendada es crear un archivo con el nombre org.hibernate.boot.model.FunctionContributor en el directorio resources/META-INF/services.
Como contenido del archivo, simplemente pon la implementación posjsonhelper
del tipo org.hibernate.boot.model.FunctionContributor
.
com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor
La solución alternativa es utilizar el componente com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher
durante el arranque de la aplicación, como en el siguiente ejemplo con el uso 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());
}
}
Para obtener más detalles, por favor consulta “Cómo adjuntar FunctionContributor.”
Modelo Ejemplo
Nuestro modelo se parece al ejemplo a continuación:
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!: En este ejemplo, la propiedad JsonbContent
es un tipo personalizado (como se muestra a continuación), pero también podría ser del 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;
// Métodos Setter y Getter
}
Operaciones DDL para la tabla:
create table item (
id bigint not null,
jsonb_content jsonb,
primary key (id)
)
A efectos de presentación, supongamos que nuestra base de datos contiene tales registros:
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 sin propiedades, solo un json vacío
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');
-- valores enteros
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}');
-- valores dobles
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}');
-- valores de enumeración
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"}');
-- valores de cadena
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"}');
-- elementos internos
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"]}}');
Usando Componentes de Criterios
A continuación se muestra un ejemplo de la misma consulta presentada al principio, pero creada con componentes SQM y el generador de criterios:
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 a generar el código SQL como el siguiente:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''
La función jsonb_extract_path_text
de Postgres es equivalente al operador #>>
(por favor consulte la documentación de Postgres vinculada anteriormente para obtener más detalles).
Operaciones en Arrays
La biblioteca soporta algunos operadores de funciones JSON de Postgres, como:
?&
– Esto verifica si todas las cadenas en el arreglo de texto existen como claves de nivel superior o elementos de arreglo. Así que generalmente si tenemos una propiedad JSON que contiene un arreglo, entonces puedes verificar si contiene todos los elementos que estás buscando.?|
– Esto verifica si alguna de las cadenas en el arreglo de texto existe como claves de nivel superior o elementos de arreglo. Así que generalmente si tenemos una propiedad JSON que contiene un arreglo, entonces puedes verificar si contiene al menos alguno de los elementos que estás buscando.
Además de ejecutar consultas SQL nativas, Hibernate 6 no tiene soporte para las operaciones mencionadas arriba.
Cambios Requeridos en DDL
El operador mencionado no puede ser utilizado en HQL debido a caracteres especiales. Es por eso que necesitamos envolverlos, por ejemplo, en una función SQL personalizada. Posjsonhelper
la biblioteca requiere dos funciones SQL personalizadas que envuelvan esos operadores. Para la configuración predeterminada, estas funciones tendrán la implementación a continuación.
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;
Para obtener más información sobre cómo personalizar o agregar programáticamente los cambios DDL requeridos, consulte la sección “Aplicar cambios DDL“.
“?&” Envoltorio
El siguiente ejemplo de código ilustra cómo crear una consulta que examina registros para los cuales la propiedad JSON que contiene una matriz tiene todos los elementos de cadena que estamos utilizando para buscar.
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();
}
En caso de que los tags contengan dos elementos, entonces Hibernate generaría el siguiente 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[?,?])
“?|” Envoltorio
El código en el ejemplo a continuación ilustra cómo crear una consulta que examina registros para los cuales la propiedad JSON contiene una matriz y tiene al menos un elemento de cadena que estamos utilizando para buscar.
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();
}
En caso de que los tags contengan dos elementos, entonces Hibernate generaría SQL como se muestra a continuación:
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[?,?])
Para ver más ejemplos de cómo usar los operadores numéricos, consulte la demostración objeto dao y pruebas dao.
¿Por qué usar la biblioteca posjsonhelper cuando Hibernate tiene algún soporte para atributos JSON de consulta
Además de esos dos operadores que admiten los tipos de matriz mencionados anteriormente, la biblioteca tiene dos operadores adicionales útiles. El jsonb_extract_path
y jsonb_extract_path_text
son envolturas para los operadores #>
y #>>
. Hibernate admite el operador ->>
. Para ver la diferencia entre esos operadores, consulte la documentación de Postgres vinculada anteriormente.
Sin embargo, como leíste al comienzo del artículo, el soporte nativo para consultas de atributos JSON solo está permitido cuando la clase JSON tiene propiedades con tipos simples. Y lo más importante, no puedes consultar por atributo si no está mapeado a la propiedad en el tipo JSON. Eso podría ser un problema si supones que tu estructura JSON puede ser más dinámica y tener una estructura elástica no definida por ningún esquema.
Con el operador posjsonhelper
, no tienes este problema. Puedes consultar por cualquier atributo que desees. No tiene que estar definido como una propiedad en el tipo JSON. Además, la propiedad en nuestra entidad que almacena la columna JSON no tiene que ser un objeto complejo como JsonbContent
en nuestros ejemplos. Puede ser una cadena simple en Java.
Conclusión
Como se mencionó en el artículo anterior, en algunos casos, los tipos y funciones JSON de Postgres pueden ser buenas alternativas para bases de datos NoSQL. Esto podría ahorrarnos la decisión de agregar soluciones NoSQL a nuestra pila tecnológica, lo que también podría agregar más complejidad y costos adicionales.
Eso también nos da flexibilidad cuando necesitamos almacenar datos no estructurados en nuestra base relacional y la posibilidad de consultar en esas estructuras.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6