Funções JSON do Postgres com Hibernate 6

Este é uma continuação do artigo anterior onde foi descrito como adicionar suporte para as funções JSON do Postgres e usar o Hibernate 5. Neste artigo, focaremos em como usar operações JSON em projetos que utilizam o framework Hibernate com a versão 6. 

Suporte Nativo

O Hibernate 6 já possui algum bom suporte para consulta por atributos JSON, como o exemplo a seguir demonstra.

Temos nossa classe de entidade normal que possui uma propriedade JSON:

Java

 

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;
    }
}

O tipo JsonbContent se parece com o seguinte:

Java

 

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 e Setters
}

Quando temos um modelo assim, podemos, por exemplo, consultar pelo atributo string_value.

Java

 

    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! – Atualmente, parece haver alguma limitação com o suporte à consulta por atributos, que é que não podemos consultar por tipos complexos como arrays. Como você pode ver, o tipo JsonbContent possui a anotação Embeddable, o que significa que se você tentar adicionar alguma propriedade que seja uma lista, podemos ver uma exceção com a seguinte mensagem: O tipo que deveria ser serializado como JSON não pode ter tipos complexos como suas propriedades: Componentes agregados atualmente só podem conter valores simples básicos e componentes de valores básicos simples. 

No caso em que o nosso tipo JSON não precisa ter propriedades com um tipo complexo, o suporte nativo é suficiente. 

Por favor, verifiquem os links abaixo para mais informações:

No entanto, às vezes vale a pena ter a possibilidade de consultar por atributos de array. Claro, podemos usar consultas SQL nativas no Hibernate e usar funções JSON do Postgres, que foram apresentadas no artigo anterior. Mas também seria útil ter essa possibilidade nas consultas HQL ou ao usar predicados programaticamente. Essa segunda abordagem é ainda mais útil quando você deve implementar a funcionalidade de uma consulta dinâmica. Embora concatenar dinamicamente uma string que deve ser uma consulta HQL possa ser fácil, a melhor prática seria usar predicados implementados. É aqui que o uso da biblioteca posjsonhelper se torna útil.

Posjsonhelper

O projeto está presente no repositório central Maven, facilitando sua adição ao incluir como uma dependência em seu projeto Maven.

XML

 

<dependency>
            <groupId>com.github.starnowski.posjsonhelper</groupId>
            <artifactId>hibernate6</artifactId>
            <version>0.2.1</version>
</dependency>

Registrar FunctionContributor

Para utilizar a biblioteca, é necessário anexar o componente FunctionContributor. Isso pode ser feito de duas maneiras. A primeira e mais recomendada é criar um arquivo com o nome org.hibernate.boot.model.FunctionContributor no diretório resources/META-INF/services

O conteúdo do arquivo deve ser a implementação posjsonhelper do tipo org.hibernate.boot.model.FunctionContributor.

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

A solução alternativa é usar o componente com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher durante o início da aplicação, conforme exemplo abaixo com o uso do Spring Framework.

Java

 

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 mais detalhes, consulte “Como anexar FunctionContributor.”

Exemplo de Modelo

Nosso modelo se assemelha ao exemplo abaixo:

Java

 

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!: Neste exemplo, a propriedade JsonbConent é um tipo personalizado (conforme descrito), mas também poderia ser do tipo String.

Java

 

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 e Getters
}

Operações DDL para a tabela:

SQL

 

create table item (
        id bigint not null,
        jsonb_content jsonb,
        primary key (id)
    )

Para fins de apresentação, vamos assumir que nosso banco de dados contém tais registros:  

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"]}');

-- item sem propriedades, apenas um json vazio
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

-- valores inteiros
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 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}');

-- valores de enumeração
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 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"}');

-- 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 Critérios

A seguir, um exemplo do mesmo query apresentado no início, mas criado com componentes SQM e construtor de critérios:

Java

 

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();
    }

O Hibernate vai gerar o código SQL como abaixo:

SQL

 

select
        i1_0.id,
        i1_0.jsonb_content 
    from
        item i1_0 
    where
        jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''

O jsonb_extract_path_text é uma função do Postgres que é equivalente ao operador #>> (por favor, consulte a documentação do Postgres vinculada anteriormente para mais detalhes).

Operações em Arrays

A biblioteca suporta alguns operadores de funções JSON do Postgres, como:

  • ?& – Isso verifica se todas as strings no array de texto existem como chaves de nível superior ou elementos do array. Então, geralmente, se temos uma propriedade JSON que contém um array, então você pode verificar se ele contém todos os elementos que você está pesquisando.
  • ?| – Isso verifica se alguma das strings no array de texto existem como chaves de nível superior ou elementos do array. Então, geralmente, se temos uma propriedade JSON que contém um array, então você pode verificar se ele contém pelo menos alguns dos elementos que você está pesquisando.

Além de executar consultas SQL nativas, o Hibernate 6 não tem suporte para as operações acima.

Alterações DDL Necessárias

O operador acima não pode ser usado em HQL devido a caracteres especiais. É por isso que precisamos envolvê-los, por exemplo, em uma função SQL personalizada. Posjsonhelper a biblioteca requer duas funções SQL personalizadas que envolverão esses operadores. Para a configuração padrão, essas funções terão a implementação abaixo.

SQL

 

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 obter mais informações sobre como personalizar ou adicionar programaticamente as DDL necessárias, consulte a seção “Aplicar alterações DDL“.

“?&” Wrapper

O exemplo de código abaixo ilustra como criar uma consulta que analisa registros para os quais a propriedade JSON que contém uma matriz possui todos os elementos de string que estamos usando para pesquisar. 

Java

 

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();
    }

Caso os tags contenham dois elementos, o Hibernate geraria o SQL abaixo:

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[?,?])

“?|” Wrapper

O código no exemplo abaixo ilustra como criar uma consulta que analisa registros para os quais a propriedade JSON contém uma matriz e possui pelo menos um elemento de string que estamos usando para pesquisar.

Java

 

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();
    }

Caso os tags contenham dois elementos, o Hibernate geraria o SQL como abaixo:

SQL

 

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 mais exemplos de como usar operadores numéricos, verifique a demo objeto dao e testes dao.

Por que usar a biblioteca posjsonhelper quando o Hibernate já oferece algum suporte para atributos JSON de consulta

Além desses dois operadores que suportam tipos de array mencionados acima, a biblioteca possui dois operadores adicionais úteis. O jsonb_extract_path e o jsonb_extract_path_text são wrappers para os operadores #> e #>>. O Hibernate suporta o operador ->>. Para ver a diferença entre esses operadores, por favor, consulte a documentação do Postgres vinculada anteriormente.

No entanto, como você leu no início do artigo, o suporte nativo para consultas em atributos JSON só é permitido quando a classe JSON possui propriedades com tipos simples. E, mais importante, você não pode consultar por atributo se ele não estiver mapeado para a propriedade no tipo JSON. Isso pode ser um problema se você assumir que a estrutura JSON pode ser mais dinâmica e ter uma estrutura elástica não definida por nenhum esquema. 

Com o operador posjsonhelper, você não enfrenta esse problema. Você pode consultar por qualquer atributo que desejar. Não é necessário que ele seja definido como uma propriedade no tipo JSON. Além disso, a propriedade em nossa entidade que armazena a coluna JSON não precisa ser um objeto complexo como JsonbConent em nossos exemplos. Pode ser uma simples string em Java.

Conclusão

Como mencionado no artigo anterior, em alguns casos, os tipos e funções JSON do Postgres podem ser boas alternativas para bancos de dados NoSQL. Isso poderia nos poupar da decisão de adicionar soluções NoSQL à nossa pilha tecnológica, o que poderia também adicionar mais complexidade e custos adicionais.

Isso também nos dá flexibilidade quando precisamos armazenar dados não estruturados em nossa base relacional e a possibilidade de consultar nessas estruturas.

Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6