Funzioni JSON di Postgres con Hibernate 5

Il database Postgres supporta alcuni tipi JSON e operazioni speciali per tali tipi.

In alcuni casi, tali operazioni potrebbero essere una buona alternativa per database a documenti come MongoDB o altri database NoSQL. Naturalmente, database come MongoDB potrebbero avere processi di replicazione migliori, ma questo argomento è al di fuori dello scopo di questo articolo.

In questo articolo, ci concentreremo su come utilizzare le operazioni JSON in progetti che utilizzano il framework Hibernate con la versione 5.

Modello di esempio

Il nostro modello è simile all’esempio seguente:

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!: Potremmo utilizzare un tipo JSON specifico per la proprietà jsonbContent, ma nella versione 5 di Hibernate, ciò non offrirebbe alcun vantaggio dal punto di vista delle operazioni.

Operazione DDL:

SQL

 

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

Per motivi di presentazione, supponiamo che il nostro database contenga tali record:

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 senza alcuna proprietà, solo un json vuoto
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

-- valori int
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}');

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

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

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

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

Approccio Query Native

In Hibernate 5, possiamo utilizzare un approccio nativo dove eseguiamo un comando SQL diretto.

Importante!: Per favore, a scopo di presentazione, omettete il fatto che il codice seguente permette l’iniezione SQL per l’operatore LIKE. Ovviamente, per tali azioni, dovremmo utilizzare parametri 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();
    }

Nell’esempio sopra, c’è l’uso dell’operatore #>> che estrae il sott-oggetto JSON alla path specificata come testo (si prega di controllare la documentazione di Postgres per maggiori dettagli).

In molti casi, una tale query (ovviamente, con un valore scappato) è sufficiente. Tuttavia, se abbiamo bisogno di implementare la creazione di una sorta di query dinamica basata sui parametri passati nella nostra API, sarebbe meglio una sorta di builder di criteri.

Posjsonhelper

Hibernate 5 per impostazione predefinita non ha supporto per le funzioni Postgres JSON. Fortunatamente, puoi implementarle da solo o utilizzare la libreria posjsonhelper che è un progetto open-source.

Il progetto è presente nel repository centrale di Maven, pertanto puoi aggiungerlo facilmente come dipendenza al tuo progetto Maven.

XML

 

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

Per utilizzare la libreria posjsonhelper nel tuo progetto, è necessario utilizzare il dialect di Postgres implementato nel progetto. Ad esempio:

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

Nel caso in cui il tuo progetto abbia già una classe di dialetto personalizzata, è possibile anche utilizzare:

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

Utilizzo di Componenti di Criteri

L’esempio seguente presenta un comportamento simile all’esempio precedente che utilizzava una query nativa. Tuttavia, in questo caso, utilizzeremo un builder di criteri.

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 genererà il codice SQL come segue:

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 funzione jsonb_extract_path_text è una funzione di Postgres equivalente all’operatore #>> (consulta la documentazione di Postgres linkata in precedenza per maggiori dettagli).

Operazioni su Array

La libreria supporta alcuni operatori di funzioni JSON di Postgres come:

  • ?& – Verifica se tutti i stringhe nell’array di testo esistono come chiavi di primo livello o elementi dell’array. Quindi, generalmente, se abbiamo una proprietà JSON che contiene un array, possiamo verificare se contiene tutti gli elementi che stiamo cercando.
  • ?| – Verifica se almeno una delle stringhe nell’array di testo esiste come chiave di primo livello o elemento dell’array. Quindi, generalmente, se abbiamo una proprietà JSON che contiene un array, possiamo verificare se contiene almeno uno degli elementi che stiamo cercando.

Modifiche DDL richieste

L’operatore sopra non può essere utilizzato in HQL a causa di caratteri speciali. Ecco perché abbiamo bisogno di incapsularli, ad esempio, in una funzione SQL personalizzata. La libreria Posjsonhelper richiede due funzioni SQL personalizzate che incapsuleranno quegli operatori. Per la configurazione predefinita, queste funzioni avranno la seguente implementazione.

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;

Per ulteriori informazioni su come personalizzare o aggiungere in modo programmatico le DDL richieste, consultare la sezione “Applica modifiche DDL“.

“?&” Wrapper

Il seguente esempio di codice illustra come creare una query che esamina i record per i quali la proprietà JSON che contiene un array ha tutti gli elementi stringa che stiamo cercando.

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

Nel caso in cui i tag contenessero due elementi, Hibernate genererebbe il seguente 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

“?|” Wrapper

Il seguente esempio di codice illustra come creare una query che esamina i record per i quali la proprietà JSON che contiene un array ha almeno un elemento stringa che stiamo cercando.

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

Nel caso in cui i tag contenessero due elementi, Hibernate genererebbe il seguente 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

Per ulteriori esempi su come utilizzare gli operatori numerici, consultare la demo oggetto dao e test dao.

Conclusione

In alcuni casi, i tipi JSON e le funzioni di Postgres possono essere buone alternative ai database NoSQL. Ciò potrebbe evitare la necessità di aggiungere soluzioni NoSQL alla nostra pila tecnologica, che potrebbe anche aggiungere maggiore complessità e costi aggiuntivi.

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