Как расширить JPA для работы с PostgreSQL

от автора

Всем привет! Меня зовут Антон, я — архитектор компании ITFB Group. Пережив несколько проектов, на которых встречается стек PostgreSQL с использованием связки PostgreSQL + JPA, мне удалось устранить большое количество проблем, связанных с неоптимальной интеграцией функциональности PostgresSQL в Java-приложениях. Вот что послужило мотивом к написанию данной статьи:

  • неверное понимание относительно стека для небольших задач от бизнеса, так называемый «банальный overengineering»;

  • зачастую неоправданное использование конкатенации строки для «сборки» запроса и насаждение из «макарон» в коде;

  • изобретение собственных велосипедов в JPA и Hibernate.

Для примера мы возьмем две функциональности PostgreSQL, они же типы данных, — tsquery и JSONB.

В бой пойдем со стеком:

  • Hibernate 6.x

  • SpringDataJPA3.+

  • PostgreSQL 15+

В этой cтатье мы максимально подробно разберем, как можно настроить JPA для эффективной работы с PostgreSQL. Всем, кому интересна эта тема, добро пожаловать под кат).

Что не так с JPA

JPA, а также его самая известная имплементация Hibernate, является общей спецификацией/библиотекой, разработанной для взаимодействия с различными реляционными базами данных. Однако есть определенные функции, которые не поддерживаются нативно в JPA при работе с PostgreSQL. Как пример, есть JSONB из коробки Hibernate. У нас существует аннотация @JdbcTypeCode(SqlTypes.JSON) и, в принципе, всё. Если вы хотите использовать нативные операторы и методы JSONB, вы можете написать нативные SQL-запросы. Однако есть предложения, которые могут улучшить функциональность с помощью доступных средств Hibernate.

Одним из ключевых классов, на который следует обратить внимание при расширении
JPA для PostgreSQL, является Dialect. Dialect действует как адаптер для трансляции «общего» синтаксиса SQL-запроса (он же — дериватив JPQL/HQL) к специфичному синтаксису SQL-запроса конкретной СУБД и предоставляется Hibernate через его свойства.

С чем придется работать при расширении Dialect

Интерфейс FunctionContributions — помогает зарегистрировать пользовательскую реализацию функции HQL.

Интерфейс TypeContributions — регистрирует пользовательские типы в вашем приложении.

JdbcType и JavaType — интерфейсы, которые помогают описать сериализацию и десериализацию пользовательских типов из POJO в параметры запросов, значения полей и обратно.

В этой статье мы не затронем нижеперечисленные инструменты, но будем помнить, что они есть в нашем арсенале:

  1. Statement interceptors. Вступает в игру, когда вам необходимо перехватить специфичную для БД строку SQL перед flush-ом и внести в нее изменения, если это необходимо. Вы можете вернуть измененную строку SQL с изменениями или без них.

  2. Query rewriter. Новая возможность в Spring Data, работает аналогично Statement interceptors.

  3. Attribute converter. Полезна для простых случаев, таких как преобразование строки в логический тип (boolean) и наоборот и т. д.

Mapping своими руками в Hibernate 6

Давайте разберем, как сопоставить PostgreSQL с типом Java/POJO. Для этого у нас есть два основных объекта библиотеки:

  1. JdbcType. Определяет тип данных, используемый для передачи параметров в PreparedStatement и извлечения значений из ResultSet или вызываемого метода.

  2. JavaType. Дополняет JdbcType и определяет методы для обертки значения в тип данных, которые будут переданы в PreparedStatement в качестве параметра запроса, а также помогает привести значения из ResultSet к конкретному значению модели/POJO.

При разработке mapping-типов придется открыть документацию PostgreSQL и постараться перенести указанный синтаксис на логику извлечения и упаковки данных в запросе.

Пример с JSONB

Предположим, что нам нужно использовать тип JSONB без использования внешних библиотек или подходов, предоставляемых «из коробки».

Напишем свой JavaType:

Мы используем java.util.Map как тип значения на стороне Java, так как для нашего тестового случая мы ожидали, что значение JSON будет хранить произвольные данные.

Для передачи POJO в качестве параметра запроса используем Jackson для сериализации объекта в json-подобную строку и оборачиваем полученную строку в PGobject.

Далее зададимся вопросом: каким образом мы получим значение из ResultSet? Для нашего случая подходящим типом будет строка или binary stream, и для десериализации воспользуемся методом getExtractor в JdbcType.

В приведенном ниже классе мы пытаемся описать функцию, которая проверяет, включает ли значение JSONB справа значение слева, используя синтаксис JSON-пути. В аргументах AST у нас есть дело с двумя типами параметров:

  • QueryLiteral — типы аргументов, передаваемый в виде постоянной строки в Criteria Builder и используемый в спецификации.

  • SqmParameterInterpretation — типы аргументов, используемые для запросов в методе с аннотацией Query в JpaRepository, который содержит параметр запроса.

После описания функции давайте зарегистрируем ее в Dialect:

Для внедрения кастомизированной Dialect есть несколько способов:

  • YAML-файл конфигурации и JPA Properties:

  • Можно воспользоваться HibernatePropertiesCustomizer:

  • И третий способ — для Spring-приложений:

Дополнительно к вышесказанному в спецификациях мы можем использовать следующие синтаксические конструкции:

  • прямую вставку SQL-кода в вашей спецификации:

  • SqmSelfRenderingExpression:

Второй этап завершен, и мы готовы к написанию и тестированию наших запросов. В качестве примера давайте воспользуемся новой функцией в спецификации Criteria API:

В JPA-запросе для работы с JSON нам нужно выполнить незначительный трюк:

Предлагаю закрепить материал и добавить в наше приложение немного tsquery. На входе у нас есть такой нативный SQL-запрос:

Этот запрос выполняет полнотекстовый поиск по фразе, отдельному слову и его синонимам.

Чтобы настроить окружение БД для полнотекстового поиска, нам нужно выполнить некоторые действия на стороне PostgreSQL.

Загрузите файлы словаря и преобразуйте их:

Поместите эти файлы в указанную папку на хосте PostgreSQL:

Запустите инициализирующий скрипт для создания словаря:

Базовые вещи мы сделали на стороне БД. Давайте опишем нашу функцию:

Далее нам необходимо зарегистрировать описание функции в диалекте:

Создайте репозиторий JPA с методом tsquery для нашего примера:

Время погонять код:

Надеюсь, что мой опыт может пригодиться на ваших проектах. Пример кода предлагаю вам посмотреть на GitGub.

Антон, архитектор компании ITFB Group


ссылка на оригинал статьи https://habr.com/ru/articles/825406/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *