Postgres JSON-functies met Hibernate 5

Postgres database ondersteunt een paar JSON typen en speciale operaties voor die typen.

In sommige gevallen kunnen deze operaties een goed alternatief zijn voor document databases zoals MongoDB of andere NoSQL databases. Natuurlijk kunnen databases zoals MongoDB betere replicatieprocessen hebben, maar dit onderwerp valt buiten het bereik van dit artikel.

In dit artikel zullen we ons richten op het gebruik van JSON-operaties in projecten die Hibernate framework gebruiken met versie 5.

Voorbeeld Model

Ons model ziet er als volgt uit:

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

Belangrijk!: We konden een specifiek JSON-type gebruiken voor de jsonbContent eigenschap, maar in Hibernate versie 5 zou dat geen voordelen bieden vanuit een operationeel standpunt.

DDL operatie:

SQL

 

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

Ter illustratie nemen we aan dat onze database zulke records bevat:

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 zonder eigenschappen, gewoon een leeg json
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

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

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

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

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

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

Native Query Approach

In Hibernate 5 kunnen we een native aanpak gebruiken waarbij we een directe SQL-opdracht uitvoeren.

Belangrijk!: Voor presentatiedoeleinden, vermeldt u niet dat de onderstaande code SQL-injectie toestaat voor de expressie voor de LIKE operator. Natuurlijk zouden we voor dergelijke actie parameters moeten gebruiken en 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();
    }

In bovenstaand voorbeeld wordt de #>> operator gebruikt die het JSON sub-object op de opgegeven pad als tekst extraheert (kijk voor meer details in de Postgres documentatie).

In de meeste gevallen is zo’n query (natuurlijk met een ontsnapte waarde) voldoende. Als we echter een soort dynamische query moeten implementeren op basis van parameters die in onze API zijn doorgegeven, is het beter om een soort criteria builder te gebruiken.

Posjsonhelper

Hibernate 5 ondersteunt standaard geen Postgres JSON functies. Gelukkig kun je het zelf implementeren of de posjsonhelper bibliotheek gebruiken, die een open source project is.

Het project bestaat in de Maven centrale repository, dus je kunt het gemakkelijk toevoegen door het als afhankelijkheid aan je Maven project toe te voegen.

XML

 

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

Om de posjsonhelper bibliotheek in je project te gebruiken, moet je de Postgres-dialect gebruiken die in het project is geïmplementeerd. Bijvoorbeeld:

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

Als je project al een aangepast dialect-klasse heeft, is er ook de mogelijkheid om te gebruiken:

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

Gebruik van Criteria Components

Het onderstaande voorbeeld vertoont een soortgelijk gedrag als het vorige voorbeeld dat een native query gebruikte. Echter, in dit geval gaan we een criteria builder gebruiken.

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 gaat de SQL-code genereren zoals hieronder:

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 ?

De jsonb_extract_path_text is een Postgres-functie die equivalent is aan de #>> operator (zie de eerder genoemde Postgres documentatie voor meer details).

Operaties op Arrays

De bibliotheek ondersteunt enkele Postgres JSON functie operators zoals:

  • ?& – Controleert of alle tekens in de tekstarray bestaan als top-level sleutels of array elementen. Dus over het algemeen, als we een JSON-eigenschap hebben die een array bevat, kun je controleren of deze alle elementen bevat die je zoekt.
  • ?| – Controleert of een van de tekens in de tekstarray bestaat als top-level sleutels of array elementen. Dus over het algemeen, als we een JSON-eigenschap hebben die een array bevat, kun je controleren of deze minstens één van de elementen bevat die je zoekt.

Vereiste DDL-wijzigingen

De operator hierboven kan niet worden gebruikt in HQL vanwege speciale tekens. Daarom moeten we ze omsluiten, bijvoorbeeld in een aangepaste SQL-functie. De Posjsonhelper bibliotheek vereist twee aangepaste SQL-functies die deze operatoren omsluiten. Voor de standaardinstelling zullen deze functies de onderstaande implementatie hebben.

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;

Ga voor meer informatie over het aanpassen of programmatisch toevoegen van vereiste DDL naar de sectie “Toepassen van DDL-wijzigingen.”

“?&” Wrapper

Het onderstaande codevoorbeeld illustreert hoe u een query kunt maken die kijkt naar records waarvan de JSON-eigenschap die een array bevat, alle string elementen bevat die we zoeken.

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

Als de tags twee elementen zouden bevatten, zou Hibernate de onderstaande SQL genereren:

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

Het onderstaande codevoorbeeld illustreert hoe u een query kunt maken die kijkt naar records waarvan de JSON-eigenschap die een array bevat, minstens één string element bevat dat we zoeken.

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

Als de tags twee elementen zouden bevatten, zou Hibernate de onderstaande SQL genereren:

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

Voor meer voorbeelden van het gebruik van numerieke operatoren, verwijs ik je graag naar de demo dao object en dao tests.

Conclusie

In sommige gevallen kunnen Postgres JSON-typen en -functies goede alternatieven zijn voor NoSQL-databases. Dit kan ons helpen besluiten om geen NoSQL-oplossingen aan onze technologie-stack toe te voegen, wat ook meer complexiteit en extra kosten zou kunnen opleveren.

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