![](https://habrastorage.org/getpro/habr/upload_files/664/73a/386/66473a386e90362e9dfe2f21977a3a8f.png)
Всем привет! Меня зовут Антон, я — архитектор компании ITFB Group. Пережив несколько проектов, на которых встречается стек PostgreSQL с использованием связки PostgreSQL + JPA, мне удалось устранить большое количество проблем, связанных с неоптимальной интеграцией функциональности PostgresSQL в Java-приложениt. Вот что послужило мотивом к написанию данной статьи:
неверное понимание относительно стека для небольших задач от бизнеса, так называемый «банальный 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.
![](https://habrastorage.org/getpro/habr/upload_files/b13/e9f/899/b13e9f899a85feed527c2c714c4a7d8c.png)
Интерфейс TypeContributions — регистрирует пользовательские типы в вашем приложении.
![](https://habrastorage.org/getpro/habr/upload_files/684/8cb/d62/6848cbd627a2e35b184d7390c852d291.png)
JdbcType и JavaType — интерфейсы, которые помогают описать сериализацию и десериализацию пользовательских типов из POJO в параметры запросов, значения полей и обратно.
В этой статье мы не затронем нижеперечисленные инструменты, но будем помнить, что они есть в нашем арсенале:
Statement interceptors. Вступает в игру, когда вам необходимо перехватить специфичную для БД строку SQL перед flush-ом и внести в нее изменения, если это необходимо. Вы можете вернуть измененную строку SQL с изменениями или без них.
Query rewriter. Новая возможность в Spring Data, работает аналогично Statement interceptors.
Attribute converter. Полезна для простых случаев, таких как преобразование строки в логический тип (boolean) и наоборот и т. д.
Mapping своими руками в Hibernate 6
Давайте разберем, как сопоставить PostgreSQL с типом Java/POJO. Для этого у нас есть два основных объекта библиотеки:
JdbcType. Определяет тип данных, используемый для передачи параметров в PreparedStatement и извлечения значений из ResultSet или вызываемого метода.
JavaType. Дополняет JdbcType и определяет методы для обертки значения в тип данных, которые будут переданы в PreparedStatement в качестве параметра запроса, а также помогает привести значения из ResultSet к конкретному значению модели/POJO.
При разработке mapping-типов придется открыть документацию PostgreSQL и постараться перенести указанный синтаксис на логику извлечения и упаковки данных в запросе.
Пример с JSONB
Предположим, что нам нужно использовать тип JSONB без использования внешних библиотек или подходов, предоставляемых «из коробки».
Напишем свой JavaType:
![](https://habrastorage.org/getpro/habr/upload_files/7ba/bc4/28d/7babc428d9097ade5cf7400398b21391.png)
Мы используем java.util.Map как тип значения на стороне Java, так как для нашего тестового случая мы ожидали, что значение JSON будет хранить произвольные данные.
Для передачи POJO в качестве параметра запроса используем Jackson для сериализации объекта в json-подобную строку и оборачиваем полученную строку в PGobject.
![](https://habrastorage.org/getpro/habr/upload_files/e09/5cf/c0d/e095cfc0d1d8a96944d640bcb07de3aa.png)
Далее зададимся вопросом: каким образом мы получим значение из ResultSet? Для нашего случая подходящим типом будет строка или binary stream, и для десериализации воспользуемся методом getExtractor в JdbcType.
В приведенном ниже классе мы пытаемся описать функцию, которая проверяет, включает ли значение JSONB справа значение слева, используя синтаксис JSON-пути. В аргументах AST у нас есть дело с двумя типами параметров:
QueryLiteral — типы аргументов, передаваемый в виде постоянной строки в Criteria Builder и используемый в спецификации.
SqmParameterInterpretation — типы аргументов, используемые для запросов в методе с аннотацией Query в JpaRepository, который содержит параметр запроса.
![](https://habrastorage.org/getpro/habr/upload_files/20b/aaa/333/20baaa33328e3e93aa2b66e2b5e1b74a.png)
После описания функции давайте зарегистрируем ее в Dialect:
![](https://habrastorage.org/getpro/habr/upload_files/453/d18/8e3/453d188e36beb4126ddb7de3c11eae70.png)
Для внедрения кастомизированной Dialect есть несколько способов:
YAML-файл конфигурации и JPA Properties:
![](https://habrastorage.org/getpro/habr/upload_files/896/502/a75/896502a75070e1b350c5a7428c5fc3b9.png)
Можно воспользоваться HibernatePropertiesCustomizer:
![](https://habrastorage.org/getpro/habr/upload_files/f33/2f7/cd8/f332f7cd8961a8a017af025d198d9175.png)
И третий способ — для Spring-приложений:
![](https://habrastorage.org/getpro/habr/upload_files/961/9a6/68c/9619a668cdfcb6e24d44aa244e867c7b.png)
Дополнительно к вышесказанному в спецификациях мы можем использовать следующие синтаксические конструкции:
прямую вставку SQL-кода в вашей спецификации:
![](https://habrastorage.org/getpro/habr/upload_files/f8a/1fc/f8f/f8a1fcf8f353d363b7827a712779e80e.png)
SqmSelfRenderingExpression:
![](https://habrastorage.org/getpro/habr/upload_files/d3c/0de/22f/d3c0de22ffceb883c320f016828b11da.png)
Второй этап завершен, и мы готовы к написанию и тестированию наших запросов. В качестве примера давайте воспользуемся новой функцией в спецификации Criteria API:
![](https://habrastorage.org/getpro/habr/upload_files/985/b78/289/985b782895e59c7183fc6faa0cb35dc6.png)
В JPA-запросе для работы с JSON нам нужно выполнить незначительный трюк:
![](https://habrastorage.org/getpro/habr/upload_files/006/c9f/f62/006c9ff628e6d7bcd682d26aaa11d37a.png)
Предлагаю закрепить материал и добавить в наше приложение немного tsquery. На входе у нас есть такой нативный SQL-запрос:
![](https://habrastorage.org/getpro/habr/upload_files/1e9/209/c74/1e9209c74ee5f5dac07e02d7ff80dd39.png)
Этот запрос выполняет полнотекстовый поиск по фразе, отдельному слову и его синонимам.
Чтобы настроить окружение БД для полнотекстового поиска, нам нужно выполнить некоторые действия на стороне PostgreSQL.
Загрузите файлы словаря и преобразуйте их:
![](https://habrastorage.org/getpro/habr/upload_files/0fe/17a/769/0fe17a769a603579d1e8f908576cd1bb.png)
Поместите эти файлы в указанную папку на хосте PostgreSQL:
![](https://habrastorage.org/getpro/habr/upload_files/0bc/bd4/800/0bcbd4800344d57d05276acd050ac814.png)
Запустите инициализирующий скрипт для создания словаря:
![](https://habrastorage.org/getpro/habr/upload_files/262/358/f04/262358f04b36bebe0ce6b4c2bf495847.png)
Базовые вещи мы сделали на стороне БД. Давайте опишем нашу функцию:
![](https://habrastorage.org/getpro/habr/upload_files/3d3/524/da5/3d3524da5d834c8e8328d1ff8cea6e13.png)
Далее нам необходимо зарегистрировать описание функции в диалекте:
![](https://habrastorage.org/getpro/habr/upload_files/526/167/b89/526167b8991654766c8148c192080f6c.png)
Создайте репозиторий JPA с методом tsquery для нашего примера:
![](https://habrastorage.org/getpro/habr/upload_files/2b6/7fa/17b/2b67fa17bba29bd873b2c9a5461b6c4a.png)
Время погонять код:
![](https://habrastorage.org/getpro/habr/upload_files/1ec/3db/4ba/1ec3db4baec4493f6601f29b7658f296.png)
Надеюсь, что мой опыт может пригодиться на ваших проектах. Пример кода предлагаю вам посмотреть на GitHub.
Антон, архитектор компании ITFB Group
Комментарии (8)
aleksandy
28.06.2024 15:17+4Судя по количеству приседаний, необходимых для того, чтобы всё это великолепие взлетело, не вижу профита по сравнению с нативными запросами.
headliner1985
28.06.2024 15:17Так в hibernate 6 специально сделали диалекты, которые да сложнее писать, зато потом будет меньше самописного шлака и велосипедов вокруг фреймворка если нужно что-то кастомное.
LaRN
28.06.2024 15:17Но если например захотете сменить фреймворк, то придется все переписывать. А с нативным sql не придется.
Sleuthhound
28.06.2024 15:17посмотреть на GitGub
Поправьте Gub, а то подумалось что что-то новенькое, ан нет
vmalyutin
28.06.2024 15:17Добрый день!
мне удалось устранить большое количество проблем, связанных с неоптимальной интеграцией функциональности PostgresSQL в Java-приложениях
Несколько раз перечитал, но понял что вы тут написали в самом конце. "В Java-приложениЕ" сразу меняет смысл.
Да и в целом язык мог бы быть гораздо проще. На много проще. Вы ж не космический корабль описываете.
Ну типа вот так. Была у нас проблема - приходилось запросы к JSONB сочинять путём конкатенации строк. Это сильно захламляло исходники. А я придумал, как это загнать это в ORM. Вы же, что там билдите, насаждаете, стеки какие-то. В общем, тяжело это читать.
Вот вы говорите, что сделали хорошо. А что собственно улучшилось не приводите. Весь смысл телодвижений вроде именно в улучшениях. А то, что вы заставили какой-то ORM плясать под вашу дуду, мне ни о чем не говорит. Я знаю огроменные приложения, где, кроме конкатенации строк нет ничего. Они прекрасно работают и в ус не дуют. И никакого бардака, кроме известного, в них нет.
А вот наоборот видел много раз. Как только ORM встречается с объёмами данных больше тестовых, всё летит к чертям собачьим.
foxyrus
Очень ̶у̶д̶о̶б̶н̶о̶ нет конечно - код в виде скриншотов.
Timur_Rodin
Так пример можно по ссылке посмотреть