Funciones JSON de Postgres con Hibernate 5

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:

Java

 

@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:

SQL

 

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:

SQL

 

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.

Java

 


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.

XML

 

        <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.

Java

 

	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:

SQL

 

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.

PLSQL

 

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.

Java

 

    
	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:

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.

Java

 

	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:

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