Я думаю первый вопрос когда речь заходит о JOOQ, что не странно, является - а зачем он нам, собственно говоря, нужен ? В большинстве случаев, и мой случай в том числе, при работе с базой данных легче всего было начать, если мы говорим о Java и Spring Boot, либо c Spring JPA, либо c Spring JDBC Template - это то что прямо из коробки нам доступно - бери и используй. Одно другому, в принципе, не мешает, и мы можем одни задачи решать быстро и удобно с помощью JPA, а другие более сложные и специфичные отдавать на выполнению Spring JDBC Template. И вот в этот гармоничный союз пытается протиснуться библиотека JOOQ.

Почему не JPA ?

Пропустив некоторую ретроспективу, мы найдем в предисловии документации самого JOOQ - Причина существования jOOQ – по сравнению с JPA

До сих пор существовало лишь несколько фреймворков или библиотек абстракции баз данных, которые по-настоящему уважали SQL как первоклассного гражданина среди языков. Большинство фреймворков, включая отраслевые стандарты JPA, EJB, Hibernate, JDO, Criteria Query и многие другие, пытаются скрыть сам SQL, сводя к минимуму его область действия до таких вещей, как JPQL, HQL, JDOQL и различные другие низшие языки запросов.

и следом

jOOQ призван заполнить этот пробел.

Мы уже конкретно понимаем к чему нас подводит создатель библиотеки, он хочет сказать что все решения, которые уже существуют на сегодняшний день в Java, ушли от самого SQL и скрыли его, а он нам нужен и JOOQ решит это.

Да, но если мы будем более конкретными, тот же Hibernate (который реализует JPA стандарт) компенсируют несоответствие между объектно-ориентированным подходом и моделью реляционной базы данных. Предоставляет очень удобные штучки к которым можно очень быстро привыкнуть.

Однако одним из наиболее сложных вариантов дизайна для фреймворка ORM является API для построения корректных и типобезопасных запросов. Тот же HQL (JPQL) лаконичен, но очевидными недостатками этого подхода являются недостаточная безопасность типов и отсутствие статической проверки запросов.

Так же сложные запросы делали разработку невыносимой и на все это как ответ появился, с версии JPA 2.0 - Criteria Query API, но читабельность кода оставляет, мягко говоря, желать лучшего, так что после чтения чужого кода по части данного API, даже "Улисс" Романа Джойса вам покажется детской сказкой...

Вот наглядный пример:

predicates.add(criteriaBuilder.or(new Predicate[] {
    criteriaBuilder.and(
              criteriaBuilder.equal(root.get("firstName"), "Олег"),
              criteriaBuilder.equal(root.get("lastName"), "Петров")
    ),
    criteriaBuilder.and(
              criteriaBuilder.equal(root.get("firstName"), "Мария"),
              criteriaBuilder.equal(root.get("lastName"), "Попова")
    )
}));

И как это могло бы выглядеть в старом добром SQL (ключевое слово - могло бы)

 ... and (first_name, last_name) IN (values('Олег', 'Петров'), values('Мария', 'Попова'))

И тут у нас по справедливости назревает вопрос: а почему бы нам не использовать просто SQL, ну как минимум когда нужно построить сложный запрос, ну посредством того же Spring JDBC Template, к примеру?

И вообще, факт существования HQL или JPQL уже сомнителен, зачем мы уходили от SQL ? С чем боролись на то и напоролись... И это не такой уж популизм я вам скажу!

А что тогда не так с SQL ?

Автор приводит список аргументов в пользу того почему "SQL можно записать в виде обычного текста и передать через JDBC API" - это не ок :

  • Нет типовой безопасности

  • Нет синтаксической безопасности

  • Чрезмерная конкатенация строк SQL

  • Скучные методы индексации значений привязки

  • Чрезмерная обработка ресурсов и исключений в JDBC

  • Не очень объектно-ориентированный JDBC API, который сложно использовать.

Стоит отметить, что главный риск это допустить синтаксическую ошибку и не обнаружить ее на этапе компиляции, но разве хорошо написанные интеграционные тесты не разрешат эту проблему ? Да что там, более того! Возможно такая опасность со стороны простого SQL скрипта будет стимулировать нас как программистов обкладывать наш код тестами более ответственно, а они в свою очередь поверх этого помогут обнаружить куда больше ошибок, которые и сам JOOQ на этапе компиляции не обнаружит ?! Ну по крайне мере, хочется верить, что это нас заставит это делать.

Та же конкатенация при правильном использовании, например, JdbcTemplate может быть в разы менее чрезмерной.

Но все же вернемся к документации в которой автор подводит итог:

SQL никогда не предназначался для абстрагирования. Быть ограниченным узкими рамками тяжелых сопоставлений, скрывая красоту и простоту реляционных данных. SQL никогда не задумывался как объектно-ориентированный. SQL никогда не предназначался для чего-то иного, кроме... SQL!

Что такое JOOQ

jOOQ (Java Object Oriented Querying) генерирует код Java из вашей базы данных и позволяет создавать типобезопасные SQL-запросы с помощью своего гибкого API.

Проблемы от которые JOOQ, по мнению официального сайта, должен нас избавить:

  • База данных в первую очередь. Устали от ORM, управляющих вашей моделью базы данных?

  • Типобезопасный SQL. Надоело обнаруживать синтаксические ошибки SQL в рабочей среде ?

  • Генерация кода. Вам надоело переименовывать имена таблиц и столбцов в коде Java ?

  • Стандартизация. Озадачены тонкими различиями в диалектах SQL ?

  • Жизненный цикл запроса. Раздражает загадочная генерация SQL вашего ORM?

  • Процедуры. Удивленый отсутствием поддержки хранимых процедур в ORM?

Подразумеватся что наш ответ на эти вопросы - да, хотя немного сомнительно, в том плане что на столько ли эти проблемы сами про себе существенны, что бы отказываться от насиженного места. Тут пусть каждый отвечает как полагает.

Но как по мне, вот к примеру, та же проблема переименовывания таблиц и полей - разве сама не является проблемой разработки самой базы, и если такое получилось, то может стоить пересмотреть способ разработки непосредственно базы. Ну ладно, что это я, может я просто не сталкивался с по-настоящему крутыми проектами где эти проблемы сплошь и рядом или наоборот...

Как использовать JOOQ

jOOQ изначально создавался как библиотека для полной абстракции JDBC

  • Типобезопасное обращение к объектам базы данных посредством сгенерированной схемы, таблицы, столбца, записи, процедуры, типа, dao, артефактов pojo

  • Типобезопасное построение SQL / построение SQL с помощью полного запроса DSL API, моделирующего SQL как предметно-ориентированный язык в Java

  • Абстракция диалекта SQL и эмуляция предложений SQL для улучшения совместимости между базами данных и включения недостающих функций в более простых базах данных

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

Как я говорил выше, мы можем использовать JPA и JDBC вместе для разных целей, так же автор в документации допускает возможность использования того же Hibernate для 70% процентов запросов и сам jOOQ для оставшихся 30%, где он действительно необходим.

Так же пусть большая и существенная часть данной библиотеки это генерация исходного кода в соответствии с базой, JOOQ можно использовать и без этого
для создания основы для динамического выполнения SQL.

Генерация кода

Как я уже говорил ранее - генерация исходного кода не является обязательной, но она является одним из основных преимуществ jOOQ, если вы хотите повысить производительность разработки.

Что нам дает генерация кода:

  • Вводите код Java непосредственно в соответствии со схемой базы данных, сохраняя всю доступную информацию о типах.

  • Когда схема вашей базы данных изменится, ваш сгенерированный код также изменится. Удаление столбцов приведет к ошибкам компиляции, которые можно обнаружить заранее.

Код можно генерировать из разных источников:

  • Непросредственно из самой базы. Необходимо подключаться к базе при генерации кода.

  • С помощью DDL на основе скриптов миграции, к примеру Flyway или Liquibase

  • На основе JPA

  • Можно создать собственный генератор.

Подробнее о генерации кода и о каждом из способов будет описано в отдельной статье

DSL API

DSL API - это основной способ создания запросов или их частей в jOOQ
jOOQ поставляется со своим собственным DSL, который эмулирует SQL на Java
Пример из документации:

SELECT *
  FROM author a
  JOIN book b ON a.id = b.author_id
 WHERE a.year_of_birth > 1920
   AND a.first_name = 'Paulo'
 ORDER BY b.title
Result<Record> result =
create.select()
      .from(AUTHOR.as("a"))
      .join(BOOK.as("b")).on(a.ID.eq(b.AUTHOR_ID))
      .where(a.YEAR_OF_BIRTH.gt(1920)
      .and(a.FIRST_NAME.eq("Paulo")))
      .orderBy(b.TITLE)
      .fetch();

Подробнее о синтаксисе, разных запросах и good practices в отдельной статье.

Диалект SQL

Как сказано в документации

Хотя jOOQ пытается максимально полно представить стандарт SQL, многие функции зависят от поставщика конкретной базы данных и ее «диалекта SQL».

То есть в этом плане мы не ограниченны и это здрово. Как пример приводится is distinct from определенный SQL:1999 и реализуемый только H2, HSQLDB и Postgres.

То есть то что у нас в SQL выглядит так:

author.A IS DISTINCT FROM author.B

И JOOQ нас в этом не ограничивает:

AUTHOR.A.isDistinctFrom(AUTHOR.B)

Так же отдельной частью автор останавливается на Oracle SQL диалекте

Oracle SQL гораздо более выразителен, чем многие другие диалекты SQL. Он содержит множество уникальных ключевых слов, предложений и функций, которые не предусмотрены стандартом SQL.
...
jOOQ имеет историческую связь с расширениями Oracle SQL. Если что-то поддерживается в Oracle SQL, то с высокой вероятностью оно попадет в jOOQ API.

QueryDSL

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

Обратимся к документации:

QueryDSL — это платформа, которая позволяет создавать статически типизированные SQL-запросы.

QueryDSL появился из-за необходимости поддерживать запросы HQL типобезопасным способом. Инкрементальное построение запросов HQL требует объединения строк и приводит к трудночитаемому коду. Небезопасные ссылки на типы и свойства домена через простые строки были еще одной проблемой при построении HQL на основе строк.

HQL для Hibernate был первым целевым языком для Querydsl, но в настоящее время он поддерживает в качестве серверных частей JPA, JDO, JDBC, Lucene, Hibernate Search, MongoDB, Collections и RDFBean.

В общем, QueryDSL создан для повышения качества работы JPA (изначально Hibernate) и как бы является альтернативой запросам JPQL и Criteria API и не претендует на большое. То есть он реализует многое чем кичится JOOQ, но в то же время, в некоторых рамках, выполняя точечную задачу. Ведь хоть и в документации JOOQ говорит что его можно использовать в 30% случаях против 70% которые можно, в силу удобства, отдать JPA, но
все равно он претендует на полную доминацию, в то время как QueryDSL именно и создан для тех 30 процентов.

С QueryDSL не работал лично, могу ошибаться в некоторых вопросах, просто советую ознакомится с данной библиотекой.

Заключение

Это была первая вводная статья о JOOQ, в которой я основываясь на документации бегло пробежался по основным позициям, и так же я планирую поэтапно более подробнее раскрыть некоторые уже упомянутые здесь части позже, а именно:

  • JOOQ. Code generation - особо важная часть библиотеки

  • JOOQ. DSL API - непосредственно то с чем больше всего нам придется работать и на что, лично я, возлагаю большие надежды

  • JOOQ. vs JPA - сравнение по всем параметрам со старым добрым JPA и не только

В общем, я сам пока не полностью осознаю JOOQ, а точнее его необходимость и его место в моей профессиональной деятельности, но данный цикл статей, как итог, будет являться и технической базой для ознакомления с особенностями JOOQ и так же его философским осмыслением и ответом на вопрос - Зачем ?

Прошу в комментариях описать свой опыт работы с библиотекой JOOQ, так как я учту это в последующих статьях.

P.S. Почему трудно понять жука ?

Что если я вам скажу что между жуком и белым движением есть некоторые параллели ?
Стоит описать позицию последних, что бы моя мысль стала яснее.

Так вот, вернем к недавней нашей общей истории и вспомним ту конъюнктуру которая сложилось на момент борьбы белого движения. В идеологическом плане у нас одной стороны левые с их красноречивыми лозунгами и манифестациями - все народу!, или как там Шариков пояснил - "Взять все, да и поделить", а с другой стороны крайне правые взывавшие в душах людей к религиозной основополагающей любви к монарху и к трепету перед пурпурной мантией. И вот чаще всего по среди этих двух таких разных, но очень конкретных сил, и находилась та блеклая позиция белого движения. Почему блеклая, вы спросите, а тут все просто - позиция левых и правых была конкретная, с чётко выраженной позицией, с ясным нарративом, а позиция тех которые понимали и чувства к устоявшемуся за тысячелетия порядку и осознавали необходимость перемен, которые и за народ и за частников, была, как говорится, ни вашим ни нашим. И многие идеи поэтому и проигрывают потому что не могут увлечь толпы этой не очевидной биполярной позицией, на подобии тех идей что имеют ярко выраженные преимущества.

И мой анализ тут крайне просто - жук в сложной ситуации, так как с одной стороны есть JPA который в своем деле очень хорош, но слишком много тянет на себя одеяла и забирает контроль, и с другой стороны определенно так же лучший в своих задачах чистый SQL, который требует от нас больше в плане вовлеченности и ответственности, и тот и другой имеет своих приверженцев, которых легко понять, их мотивы понятны любому толковому программисту, но вот тут, сначала кажущую серединную, позицию жука трудно понять... Но вот именно ради этого мы и собрались здесь - определится на какой же мы все-таки стороне.

Комментарии (16)


  1. Beholder
    27.05.2024 10:56

    На Kotlin на JPA Criteria Query пример мог бы выглядеть так:

        fun findSomeone(): List<Person> {
            return repository.findAll { root, query ->
                val firstName = root[Person_.firstName]
                val lastName = root[Person_.lastName]
                or((firstName equal "Олег") and (lastName equal "Петров"),
                   (firstName equal "Мария") and (lastName equal "Попова"))
            }
        }

    Если воспользоваться hibernate-jpamodelgen, включить context receivers и написать немного своих расширений.


    1. maxzh83
      27.05.2024 10:56
      +1

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


  1. maxzh83
    27.05.2024 10:56
    +1

    есть JPA который в своем деле очень хорош, но слишком много тянет на себя одеяла и забирает контроль

    Для таких случаев есть Spring Data Jdbc. Простые запросы не надо писать руками, достаточно просто правильно назвать метод в интерфейсе, как в Spring Data JPA. С другой стороны, у вас больше контроля и нет монструозности JPA с сессиями, контекстами, кэшами и вот этим всем.


  1. svz
    27.05.2024 10:56
    +2

    Больше года использую jooq. Пришёл к выводу, что наши запросы либо слишком простые, чтобы использовать jpa и hibernate, либо слишком сложные, чтобы использовать jpa и hibernate.

    Что понравилось в жуке: в нём сильно меньше магии и конвенций, которые нужно изучать, плюс отличная документация и автор, который отвечает на вопросы на SO.


    1. janvaljan Автор
      27.05.2024 10:56

      Какой генератор кода вы используете ? Может есть то что было удобно в JPA и чего нет в JOOQ ?


  1. Sigest
    27.05.2024 10:56
    +2

    Уже года 3 знаком с jooq и теперь везде стараюсь использовать его. Поначалу вымораживала концепция «table first», но потом, после некоторой возни с gradle и настройки регенерации сущностей по одной команде стало норм. Даже более удобно, чем в хибере/jpa, где сущности надо все же руками кодить. Конечно не хватает спринговой магии, где простые запросы можно через интерфейсный метод описать, но с другой стороны у jooq в части сложных запросов гибкости в разы больше, да и визуально легче читать.

    Один большой минус у jooq - это то что в сложных проектах логика слоя данных течёт во все соседние слои, будь то сервисный слой с БЛ, слой контроллеров. В спринг дата все же слой репозиториев более явно выделен, чем в случае с jooq, где запрос можно писать в любом месте, чем ленивые разработчики и пользуются: «да это же маленький запросик, напишу-ка я его прям в контролере, чо ходить-то за данными далеко». И получается бардак, непонятно что где лежит. Исправляется кодостайлом и дисциплиной, но все же…


    1. janvaljan Автор
      27.05.2024 10:56

      «table first» подразумевается что база данных с таблицами должна существовать на момент генерации классов или вообще в принципе необходима для старта разработки ? Или можете, пожалуйста, конкретнее описать, я отмечу это в следующей статье.


      1. Sigest
        27.05.2024 10:56

        Ну да. Вы сначала создаете бд, таблицы, хранимые запросы и т.д., а потом запускаете генерацию jooq классов, предварительно настроив сам jooq на соединение с БД. В принципе, существование бд для генерации сущностей, а потом компиляции проекта не обязательно. Я себе настроил gradle так, чтобы он во время компиляции сначала поднимал БД в testcontainers, потом флайвейем накатывал миграции и после генерил jooq классы. Это удобно, когда, например, внес изменения в flyway и хочешь сразу эти изменения увидеть во время разработки. Запустил генерацию и получил новые поля в сущностях. Доступная бд при этом не нужна, но при этом получаешь 100% рабочий код, который еще не проверен на реальной базе данных, в отличии от такого же подхода (без рабочей бд) в JPA/ Hibernate, когда написал entity руками, но мог и ошибиться в чем угодно при описании. Такой же механизм у меня работает в CI/CD. Файлы jooq я в репу не кладу, они генерятся на лету.

        Но вы говорите, что можно идти от обратного - сначала сущность руками пишем, а в бд за структурой не ходим. Я, если честно, такое не практиковал, да и вообще о таком подходе только сейчас узнал


        1. janvaljan Автор
          27.05.2024 10:56

          Просто есть разные способы генерации кода и один из них это с помощью DDL на основе скриптов миграции, к примеру Flyway или Liquibase, вот тут в документации можете ознакомится. То есть в вашем случае получается вы делаете лишнюю работу тем что поднимаете тест контейнер вместо того что бы сразу, относительно DDL скриптов, генерировать классы. Хотя и ваш способ имеет место быть.


          1. Sigest
            27.05.2024 10:56

            Прикольная конечно штука, но смущает один момент - "it applies all your DDL increments to an in-memory H2 database". По моему опыту не все SQL стейтменты разных БД могут быть отражены в H2 корректно. Сталкивался с таким несколько раз, в эпоху, когда еще testcontainers не были распространены, но в тестах приходилось использовать H2. По памяти, постгресовский тип UUID неправильно будет отражен в моих jooq классах. Вроде INT4, INT8 того же постгреса смапятся чуть по другому. Если у меня в БД еще подключены модули, например гео-типов postgis, то H2 их не поймет. Так что инструмент довольно ограниченный, хотя я могу ошибаться, не пользовался же.

            А, оказывается внизу уже ответили


        1. ermadmi78
          27.05.2024 10:56

          Я обычно на Gradle или Maven делаю пайплайн, который подымает в docker целевую СУБД, потом с помощью Flyway или Liquibase накатывает схему, а потом запускает кодогенерацию jOOQ. Пайплайн запускается отдельной командой, сгенерированный код коммитится в Git.

          Логика следующая - схема меняется не часто, поэтому при каждой сборке нет смысла запускать кодогенерацию. Когда нужно, редактирую схему, запускаю пайплайн кодогенерации, правлю зависимости, результат комичу в Git.


          1. janvaljan Автор
            27.05.2024 10:56

            Можно поинтересоваться, почему именно так ? Почему не через то же дополнение для генерации кода - DDLDatabase, который я описал выше ? Может в тот момент когда вы столкнулись с этим, еще не было реализовано данное дополнение или вы считаете, что использовать реальную базу, на которую вы накатили самостоятельно скрипты лучше чем просто относительно скриптов ?


            1. ermadmi78
              27.05.2024 10:56
              +1

              Насколько я понял, DDLDatabase завязан на H2. А в моих схемах часто встречаются специфичные для вендора СУБД конструкции - типы данных, хранимые процедуры, индексы. И мне важно, чтобы по ним генерировался корректный DSL. А DDLDatabase предлагает просто игнорировать такие конструкции - мне это не подходит.


              1. janvaljan Автор
                27.05.2024 10:56

                да, вы правы


          1. Sigest
            27.05.2024 10:56
            +2

            Все то же самое. Генерация отдельной командой, вне обычного билда. Только я не коммичу таблицы, так как запуск контейнера и генерация занимают секунд 10. Не критично мне. Как раздуется БД, так, что генерация будет идти в разы дольше, то наверное нужно будет все же складывать jooq классы в репу.


  1. edu_RDTEX
    27.05.2024 10:56

    Начинаем через 10 минут! Подключайтесь к трансляции!

    https://t.me/oraclemasters