Funções JSON do Postgres com Hibernate 5

Banco de dados Postgres suporta alguns tipos JSON e operações especiais para esses tipos.

Em alguns casos, essas operações podem ser uma boa alternativa para bancos de dados de documentos como MongoDB ou outros bancos de dados NoSQL. Claro, bancos de dados como MongoDB podem ter melhores processos de replicação, mas esse assunto está fora do escopo deste artigo.

Neste artigo, nosso foco será em como usar operações JSON em projetos que utilizam o framework Hibernate com a versão 5.

Exemplo de Modelo

Nosso modelo se assemelha ao exemplo abaixo:

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!: Poderíamos usar um tipo JSON específico para a propriedade jsonbContent, mas na versão 5 do Hibernate, isso não traria nenhum benefício do ponto de vista das operações.

Operação DDL:

SQL

 

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

Para fins de apresentação, vamos supor que nosso banco de dados contenha registros como estes:

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

Abordagem de Consulta Nativa

No Hibernate 5, podemos usar uma abordagem nativa onde executamos um comando SQL direto.

Importante!: Por favor, para fins de apresentação, omita o fato de que o código abaixo permite a injeção de SQL para a expressão para o operador LIKE. Claro, para tal ação, devemos usar parâmetros e 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();
    }

No exemplo acima, há o uso do operador #>> que extrai o sub-objeto JSON no caminho especificado como texto (por favor, consulte a documentação do Postgres para mais detalhes).

Na maioria dos casos, tal consulta (claro, com um valor escapado) é suficiente. No entanto, se precisarmos implementar a criação de algum tipo de consulta dinâmica baseada em parâmetros passados em nossa API, seria melhor algum tipo de construtor de critérios.

Posjsonhelper

O Hibernate 5, por padrão, não tem suporte para as funções Postgres JSON. Felizmente, você pode implementá-lo por conta própria ou usar a biblioteca posjsonhelper, que é um projeto de código aberto.

O projeto está disponível no repositório central do Maven, portanto, você pode adicioná-lo facilmente adicionando-o como uma dependência em seu projeto Maven.

XML

 

        <dependency>
            <groupId>com.github.starnowski.posjsonhelper</groupId>
            <artifactId>hibernate5</artifactId>
            <version>0.1.0</version>
        </dependency>

Para usar a biblioteca posjsonhelper em seu projeto, você precisa usar o dialecto Postgres implementado no projeto. Por exemplo:

com.github.starnowski.posjsonhelper.hibernate5.dialects.PostgreSQL95DialectWrapper ...

Caso seu projeto já possua uma classe de dialeto personalizado, também é possível usar:

com.github.starnowski.posjsonhelper.hibernate5.PostgreSQLDialectEnricher;

Usando Componentes de Critérios

O exemplo abaixo possui comportamento semelhante ao exemplo anterior que usou uma consulta nativa. No entanto, neste caso, vamos usar um construtor de critérios.

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

O Hibernate gerará o código SQL conforme abaixo:

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 ?

A funçã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:

  • ?& – Verifica se todos os 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, você pode verificar se ele contém todos os elementos que está pesquisando.
  • ?| – Verifica se algum dos 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, você pode verificar se ele contém pelo menos um dos elementos que está pesquisando.

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. A biblioteca Posjsonhelper 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.

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 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 examina registros para os quais a propriedade JSON que contém uma matriz possui todos os elementos de string que estamos pesquisando.

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

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

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

“?|” Wrapper

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

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

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

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 mais exemplos de como usar operadores numéricos, consulte a demonstração objeto dao e testes dao.

Conclusão

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

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