Base de datos de Postgres admite algunos tipos JSON y operaciones especiales para esos tipos.
En algunos casos, esas operaciones podrían ser una buena alternativa para bases de datos de documentos como MongoDB u otras bases de datos NoSQL. Por supuesto, las bases de datos como MongoDB podrían tener mejores procesos de replicación, pero este tema está fuera del alcance de este artículo.
En este artículo, nos enfocaremos en cómo utilizar las operaciones JSON en proyectos que utilizan el marco de Hibernate con la versión 5.
Modelo de ejemplo
Nuestro modelo se parece al ejemplo a continuación:
@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;
}
}
¡Importante!: Podríamos utilizar un tipo JSON específico para la propiedad jsonbContent
, pero en la versión 5 de Hibernate, eso no aportaría beneficios desde el punto de vista de las operaciones.
Operación DDL:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
A los efectos de la presentación, supongamos que nuestra base de datos contiene registros como estos:
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"]}}');
Enfoque de Consulta Nativa
En Hibernate 5, podemos utilizar un enfoque nativo donde ejecutamos un comando SQL directo.
Importante!: Por favor, para fines de presentación, omita el hecho de que el código a continuación permite la inyección SQL para la expresión del operador LIKE
. Por supuesto, para tal acción, deberíamos usar parámetros y 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();
}
En el ejemplo anterior, se utiliza el operador #>>
que extrae el sub-objeto JSON en el camino especificado como texto (verifique la documentación de Postgres para más detalles).
En la mayoría de los casos, tal consulta (por supuesto, con un valor escapado) es suficiente. Sin embargo, si necesitamos implementar la creación de algún tipo de consulta dinámica basada en parámetros pasados en nuestra API, sería mejor algún tipo de constructor de criterios.
Posjsonhelper
Hibernate 5, por defecto, no tiene soporte para las funciones Postgres JSON. Afortunadamente, puedes implementarlo tú mismo o utilizar la biblioteca posjsonhelper, que es un proyecto de código abierto.
El proyecto se encuentra en el repositorio central de Maven, por lo que puedes agregarlo fácilmente como una dependencia en tu proyecto de Maven.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate5</artifactId>
<version>0.1.0</version>
</dependency>
Para utilizar la biblioteca posjsonhelper
en tu proyecto, necesitas utilizar el dialecto Postgres implementado en el proyecto. Por ejemplo:
com.github.starnowski.posjsonhelper.hibernate5.dialects.PostgreSQL95DialectWrapper ...
En caso de que tu proyecto ya tenga una clase de dialecto personalizado, también existe la posibilidad de utilizar:
com.github.starnowski.posjsonhelper.hibernate5.PostgreSQLDialectEnricher;
Usando Componentes de Criterios
El ejemplo a continuación tiene un comportamiento similar al ejemplo anterior que utilizó una consulta nativa. Sin embargo, en este caso, vamos a utilizar un constructor de criterios.
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 generará el código SQL de la siguiente manera:
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 función jsonb_extract_path_text
es una función de Postgres que es equivalente al operador #>>
(consulta la documentación de Postgres vinculada anteriormente para obtener más detalles).
Operaciones en Matrices
La biblioteca admite algunos operadores de funciones JSON de Postgres como:
?&
– Verifica si todos los strings en el array de texto existen como claves de nivel superior o elementos de array. Por lo general, si tenemos una propiedad JSON que contiene un array, puedes verificar si contiene todos los elementos que estás buscando.?|
– Verifica si alguno de los strings en el array de texto existe como clave de nivel superior o elemento de array. Por lo general, si tenemos una propiedad JSON que contiene un array, puedes verificar si contiene al menos uno de los elementos que estás buscando.
Necesidades de cambios en DDL
El operador mencionado no puede utilizarse en HQL debido a caracteres especiales. Por ello, es necesario envolverlos, por ejemplo, en una función SQL personalizada. La biblioteca Posjsonhelper
requiere dos funciones SQL personalizadas que envuelvan esos operadores. Para la configuración predeterminada, estas funciones tendrán la implementación que se muestra 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 los registros para los cuales la propiedad JSON que contiene una matriz tiene todos los elementos de cadena que estamos buscando.
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();
}
En caso de que las etiquetas contengan dos elementos, Hibernate generaría la siguiente SQL:
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
“?|” Envoltorio
El siguiente ejemplo de código ilustra cómo crear una consulta que examina los registros para los cuales la propiedad JSON que contiene una matriz tiene al menos un elemento de cadena que estamos buscando.
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();
}
En caso de que las etiquetas contengan dos elementos, Hibernate generaría la siguiente SQL:
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
Para obtener más ejemplos de cómo utilizar los operadores numéricos, consulte la demostración objeto dao y pruebas dao.
Conclusión
En algunos casos, los tipos y funciones JSON de Postgres pueden ser buenas alternativas a las 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.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-5