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:
@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:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
Ter illustratie nemen we aan dat onze database zulke records bevat:
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
.
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.
<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.
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:
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.
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.
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:
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.
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:
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