Это продолжение предыдущей статьи, где описывалось, как добавить поддержку для функций JSON Postgres и использовать Hibernate 5. В этой статье мы сосредоточимся на том, как использовать операции JSON в проектах, использующих фреймворк Hibernate версии 6.
Встроенная Поддержка
Hibernate 6 уже имеет неплохую поддержку запросов по атрибутам JSON, как показано в следующем примере.
У нас есть обычный класс сущности, имеющий один JSON-свойство:
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;
}
}
Тип JsonbContent
выглядит следующим образом:
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;
//Геттеры и Сеттеры
}
Когда у нас есть такая модель, мы можем, например, запросить по атрибуту string_value
.
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();
}
Важно! – В настоящее время, похоже, существуют некоторые ограничения с поддержкой запросов по атрибутам, а именно мы не можем запрашивать по сложным типам, таким как массивы. Как видно, тип JsonbContent
имеет аннотацию Embeddable
, что означает, что если вы попытаетесь добавить свойство, которое является списком, мы можем увидеть исключение с сообщением: Тип, который должен сериализоваться как JSON, не может иметь сложные типы в качестве своих свойств: Составные компоненты в настоящее время могут содержать только простые основные значения и компоненты простых основных значений.
В случае, если наш тип JSON не требует свойств с комплексным типом, то достаточно иметь встроенную поддержку.
Пожалуйста, проверьте нижеприведенные ссылки для получения более подробной информации:
- Stack Overflow: Навигация по JSON в Hibernate 6.2
- Hibernate ORM 6.2 – Составные агрегатные отображения
- GitHub: hibernate6-tests-native-support-1
Однако иногда полезно иметь возможность запрашивать по атрибутам массива. Конечно, мы можем использовать встроенные SQL-запросы в Hibernate и использовать функции JSON в Postgres, которые были представлены в предыдущей статье. Но было бы также полезно иметь такую возможность в запросах HQL или при использовании программных предикатов. Второй подход еще более полезен, когда вам предстоит реализовать функциональность динамического запроса. Хотя динамическое соединение строки, которая должна быть запросом HQL, может показаться простым, лучшей практикой было бы использование реализованных предикатов. Вот где использование библиотеки posjsonhelper становится удобным.
Posjsonhelper
Проект существует в центральном репозитории Maven, поэтому вы можете легко добавить его, добавив его как зависимость в вашем проекте Maven.
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate6</artifactId>
<version>0.2.1</version>
</dependency>
Регистрация FunctionContributor
Для использования библиотеки необходимо присоединить компонент FunctionContributor
. Это можно сделать двумя способами. Первый и наиболее рекомендуемый — создать файл с именем org.hibernate.boot.model.FunctionContributor в директории resources/META-INF/services.
В качестве содержимого файла просто укажите posjsonhelper
— реализацию типа org.hibernate.boot.model.FunctionContributor
.
com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor
Альтернативным решением является использование компонента com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher
во время запуска приложения, как показано в примере с использованием Spring Framework.
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());
}
}
Подробнее см. “Как присоединить FunctionContributor“.
Пример модели
Наша модель выглядит следующим образом:
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;
}
}
Важно!: В данном примере свойство JsonbConent
является пользовательским типом (как показано ниже), но оно также может быть типом String.
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;
// Сеттеры и геттеры
}
Операции DDL для таблицы:
create table item (
id bigint not null,
jsonb_content jsonb,
primary key (id)
)
Для наглядности предположим, что наша база данных содержит такие записи:
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"]}');
-- элемент без каких-либо свойств, просто пустой json
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');
-- значения 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}');
-- значения 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}');
-- значения 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"}');
-- значения 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"}');
-- внутренние элементы
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"]}}');
Использование компонентов критериев
Ниже приведен пример того же запроса, представленного в начале, но созданного с использованием компонентов SQM и конструктора критериев:
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();
}
Hibernate собирается генерировать SQL-код следующим образом:
select
i1_0.id,
i1_0.jsonb_content
from
item i1_0
where
jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''
Функция jsonb_extract_path_text
в Postgres эквивалентна оператору #>>
(подробности см. в документации Postgres, ссылка на которую была предоставлена ранее).
Операции над массивами
Библиотека поддерживает несколько операторов функций JSON Postgres, таких как:
?&
– Это проверяет, существуют ли все строки в текстовом массиве в качестве ключей верхнего уровня или элементов массива. Таким образом, если у нас есть свойство JSON, содержащее массив, то можно проверить, содержит ли он все элементы, которые вы ищете.?|
– Это проверяет, существует ли хотя бы одна из строк в текстовом массиве в качестве ключа верхнего уровня или элемента массива. Таким образом, если у нас есть свойство JSON, содержащее массив, то можно проверить, содержит ли он хотя бы один из элементов, которые вы ищете.
Помимо выполнения собственных SQL-запросов, Hibernate 6 не поддерживает вышеуказанные операции.
Требуемые изменения DDL
Оператор выше не может быть использован в HQL из-за специальных символов. Вот почему их нужно оборачивать, например, в пользовательской SQL-функции. Библиотека Posjsonhelper
требует две пользовательские 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;
Для получения более подробной информации о том, как настроить или добавить программно требуемые изменения DDL, обратитесь к разделу “Применение изменений DDL.”
“?&” Обертка
Ниже приведен пример кода, иллюстрирующий, как создать запрос, который просматривает записи, для которых JSON-свойство, содержащее массив, имеет все строковые элементы, которые мы используем для поиска.
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();
}
Если теги содержат два элемента, то Hibernate сгенерирует следующий 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[?,?])
“?|” Обертка
Код в примере ниже иллюстрирует, как создать запрос, который просматривает записи, для которых JSON-свойство содержит массив и имеет хотя бы один строковый элемент, который мы используем для поиска.
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();
}
Если теги содержат два элемента, то Hibernate сгенерирует 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[?,?])
Для большего количества примеров использования числовых операторов, пожалуйста, ознакомьтесь с демонстрацией dao объекта и dao тестов.
Почему Использовать Библиотеку posjsonhelper, Когда Hibernate Поддерживает Атрибуты JSON для Запросов
Помимо двух операторов, поддерживающих массивы, упомянутые выше, библиотека имеет два дополнительных полезных оператора. jsonb_extract_path
и jsonb_extract_path_text
являются обертками для операторов #>
и #>>
. Hibernate поддерживает оператор ->>
. Чтобы увидеть разницу между этими операторами, пожалуйста, проверьте документацию Postgres, которая была ссылкой ранее.
Однако, как вы прочитали в начале статьи, поддержка нативных запросов для атрибутов JSON разрешена только тогда, когда класс JSON имеет свойства с простыми типами. И, что более важно, вы не можете запрашивать атрибут, если он не отображен на свойство в типе JSON. Это может стать проблемой, если вы предполагаете, что ваша структура JSON может быть более динамичной и иметь эластичную структуру, не определенную каким-либо схемой.
С помощью оператора posjsonhelper
у вас не возникает этой проблемы. Вы можете запрашивать любые атрибуты, которые вы хотите. Их не нужно определять как свойства в типе JSON. Более того, свойство в нашей сущности, которое хранит столбец JSON, не обязательно должно быть сложным объектом, как JsonbContent
в наших примерах. В Java это может быть простой строкой.
Заключение
Как упоминалось в предыдущей статье, в некоторых случаях типы и функции JSON Postgres могут быть хорошими альтернативами для NoSQL баз данных. Это может избавить нас от необходимости добавлять NoSQL-решения в наш технологический стек, что также может добавить больше сложности и дополнительных затрат.
Это также дает нам гибкость, когда нам нужно хранить неструктурированные данные в нашей реляционной базе, и возможность запрашивать эти структуры.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6