Большинство enterprise-приложений работают с БД в том или ином виде. Чаще всего в качестве БД выступает реляционная DBMS, например, PostgreSQL или Oracle. Относительно часто для доступа к данным используют Hibernate. Ранее он реализовывал только одну спецификацию — JPA (Java Persistence API), она же Jakarta. Но теперь Hibernate реализует ещё и Jakarta Data.

Jakarta Data — это новая спецификация под зонтиком проекта Jakarta EE (как и JPA), которая упрощает интеграцию данных в корпоративных Java-приложениях. Обе эти спецификации разрабатывает Eclipse Foundation, и в частности Gavin King, создатель Hibernate.

Большинство разработчиков привыкли работать с Hibernate именно через Spring Data JPA. Изначально, когда только шло обсуждение спецификации Jakarta Data, Spring Data (не обязательно JPA) была одним из тех проектов, который, в перспективе, мог бы реализовать спецификацию Jakarta Data. Но этого не произошло, и, несмотря на то, что изначально команда Spring Data была вовлечена в процесс создания спецификации, она отказались от идеи реализовывать Jakarta Data, и Jakarta Data стала развиваться самостоятельно. Сегодня Jakarta Data реализуют Hibernate, Open Liberty и ряд более мелких решений. Как же так вышло?

Меня зовут Михаил Поливаха, я практикующий инженер и активный коммитер Spring Data. В этой статье я расскажу об особенностях Jakarta Data, как она появилась и чем отличается от конкурентных решений. Я также расскажу, что помешало команде Spring Data реализовать Jakarta Data, и что же нас ждёт дальше.

Jakarta Data и небольшая историческая справка

Jakarta Data — это спецификация для абстрагирования слоя общения с базой данных. Иными словами, она описывает, как выглядит репозиторный слой доступа к данным (все примеры будут в секции ниже). Есть несколько реализаций JD, но нас интересует в основном Hibernate, потому что он наиболее популярен.

В мировом ИТ‑сообществе выделяются две крупные компании, которые занимают важное место в экосистеме Java разработки — Red Hat и Broadcom. Они известны следующим:

  • Broadcom. Пару лет назад купила компанию VMware и её проект VMware Tanzu, который включает в себя разработку Spring Framework. Иными словами, сейчас именно Broadcom занимается разработкой Spring-а.

  • Red Hat. Разработала альтернативный Spring-у фреймворк для создания «cloud‑native» приложений под названием Quarkus. Довольно часто Quarkus используют для последующей работы с GraalVM Native Image. Именно Red Hat является одним из основных разработчиков спецификаций проекта Jakarta EE — в том числе JPA и Jakarta Data. Опять же, de jure Jakarta EE разрабатывает консорциум компаний, который называется Eclipse Foundation. А de facto Red Hat имеет очень значительный вес в этом консорциуме, и во многом играет определяющую роль в разработке спецификаций Jakarta EE.

Итак, что мы имеем. У обеих компаний есть собственные конкурирующие продукты — Spring и Quarkus. При этом Quarkus от Red Hat в большей степени ориентирован на Jakarta‑спецификации. Иными словами, фреймворк во многом построен на спецификациях Jakarta CDI, JAX‑RS и др. Это логично, поскольку Red Hat активно их продвигает.

Также обращаю ваше внимание, что Red Hat является сегодня основным разработчиком Hibernate и проектов на его основе — hibernate‑validator (референсная реализация для JSR 380), hibernate‑reactive и др.

Давайте теперь исключительно ради примера бегло ознакомимся с тем, как выглядит код с использованием Quarkus:

Здесь используется Jakarta EE. Из примера видно, как строится взаимодействие с базой данных и как идёт внедрение зависимостей. Применены аннотации:

  • @Inject — для внедрения зависимостей;

  • @PersistenceContext — для работы с persistence контекстом (Session в рамках Hibernate);

  • @Transactional — для управления транзакциями.

Эти аннотации — часть Jakarta-спецификаций (посмотрите на import-ы). А Quarkus реализует их, поэтому мы можем использовать аннотации из Jakarta CDI и JTA в своём приложении.

Как работает Jakarta Data

Jakarta Data предоставляет интерфейс репозиториев, через которые можно взаимодействовать с хранилищем. На первый взгляд, это очень похоже на Spring Data. Jakarta Data определяет несколько ключевых интерфейсов:

  • DataRepository — интерфейс без каких‑либо методов. Похож во многом на интерфейс Repository в Spring Data.

  • BasicRepository — наследуется от DataRepository. Определяет ряд базовых методов, такие как findAll(), save(), findById(), deleteById() и др. Во многом API BasicRepository схож с тем, что предлагает CrudRepository в Spring Framework (ну путать с CrudRepository в Jakarta Data).

  • CrudRepository — наследуется от BasicRepository. Добавляет ещё определённую функциональность.

Каждый из этих интерфейсов обязывает реализацию, которая будет создана Hibernate-ом, реализовывать те или иные операции или методы по взаимодействию с базой данных (опять же, мы говорим про Jakarta Data в разрезе Hibernate, потому что Open Liberty используют довольно мало людей).

Важный момент: строгоговоря, репозиторий в Jakarta Data не обязан наследоваться вообще ни от чего. DataRepository, BasicRepository и т. д. — это лишь опциональные интерфейсы в Jakarta Data. Ваш собственный, кастомный репозиторий не обязан иметь их в наследниках. Поэтому, в Jakarta Data для обозначения, что ваш конкретный интерфейс является репозиторием, вы обязаны аннотировать его аннотацией @Repository из Jakarta Data. В Spring Framework @Repository является опциональной.

Обратите внимание, @Repository в Jakarta Data не равна @Repository в Spring Data, это разные аннотации. Ниже пример репозитория Jakarta Data:

Для этого репозитория Hibernate Annotation Processor создаст реализацию на этапе сборки. О реализации, которую создаст Hibernate, я расскажу ниже.

Структура инфтерфейса в Jakarta Data кажется относительно похожей на ту, что есть в Spring Data, и можно предположить, что спецификация призвана решить ту же проблему, что и Spring Data. Однако Mark Paluch, лидер проекта Spring Data, поделился информацией о том, что Spring Data поддерживать спецификацию Jakarta Data не будет.

Причин у этого решения несколько, мы их разберём.

Принципы работы репозиториев. Ключевая проблема

На примере кода выше мы уже можем заметить первые отличия от Spring Data. Во-первых, как известно, Spring Data строит репозиторий вокруг одной конкретной корневой сущности, с которой он работает. Класс этой сущности определяется через generic параметр у корневого интерфейса, например, у Repository. Вышеописанный пример репозитория Jakarta Data не наследуется ни от кого, что разрешено спецификацией.

Это на самом деле является первой большой проблемой для Spring Data как для технологии. Репозитории в Jakarta Data имеют право не наследовать базовый интерфейс репозитория потому, что репозиторий в Jakarta Data вообще не привязан ни к какой сущности:

Как видите, ProductRepository может работать как с тегами, так и с книгами, и с продуктами. Для Spring Data это большая проблема, он к этому не готов. Реализации Jakarta Data, например Hibernate, опираются на типы параметров или возвращаемые значения методов для того, чтобы понять, какие сущности вовлечены в запрос.

Ещё одна проблема заключается в том, что в репозиторий Spring Data — абстракция доступа к определённому набору данных в принципе, и не обязательно, что все операции репозитория будут вовлекать в работу сущность, к которой привязан репозиторий. В Jakarta Data операции, которые предоставляют репозитории, всегда оперируют сущностями (по крайней мере, на данный момент).

Для понимания: через Spring Data JPA можно вызывать хранимые процедуры, и такой вызов сам по себе не вовлекает какую‑либо сущность в процесс. Ни по параметру метода, ни по возвращаемому значению нельзя понять, какая сущность участвует в запросе:

Ещё пару слов о возвращаемом значении. В Spring Data уже давно существуют проекции (Projection), через которые можно запрашивать конкретные поля из сущности — например, только title и author, не загружая все N полей. Это улучшает производительность благодаря небольшому объёму передаваемых данных. Но если использовать Projection, то реализация Jakarta Data просто не поймёт, для какой сущности идёт запрос, потому что возвращаемое значение — это проекция, а не сущность:

Ситуация осложняется ещё и тем, что помимо обычных (PostShortPreview) и открытых (PostPreview) проекций, как в примере выше, Spring Data поддерживает сложные динамические проекции. Это особенно полезно, когда разработчик не знает заранее, какие поля ему понадобятся, и решение об использовании того или иного типа проекции принимает во время исполнения в зависимости от условий работы с данными:

Но в этом случае есть проблема: реализации репозиториев Spring Data генерируются во время работы приложения (кстати, с приходом AOT Repositories так будет не всегда, об этом я расскажу на Joker 2025), и они могут в рантайме определить тип проекции, которую надо использовать, и создать экземпляр класса проекции на лету.

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

Помимо этого, обратите внимание, в репозитории Jakarta Data есть отдельные аннотации, такие как @Insert или @Delete. В Jakarta Data типы операции, которую конкретный метод должен выполнить, как правило, определяются посредством аннотаций, как раз таки как в примере выше.

Также в Jakarta Data у нас есть ещё один метод, который ищет теги по имени. В примере ProductRepository для этого используется аннотация @Query.

Обращаю ваше внимание, что @Query в примере ProductRepository – это аннотация из Jakarta Data, а не из Spring Data. В ProductRepository в атрибуте value аннотации @Query применяется отдельный, специальный язык запросов — Jakarta Data Query Language (JDQL). Это не Hibernate Query Language (HQL) и не Jakarta Persistence Query Language (JPQL), а именно язык спецификации Jakarta Data.

Поскольку Hibernate является реализацией Jakarta Data, вы можете сделать правильный вывод, что Hibernate на данный момент может работать как с HQL и JPQL, так и с JDQL.

JDQL как новый язык запросов

Тут есть один нюанс.

Jakarta Data как спецификация не делает никаких предположений кроме того, с каким хранилищем данных она работает. Иными словами, Jakarta Data призвана абстрагировать репозиторный слой вне зависимости от типа хранилища. Hibernate, по сути, является реализацией Jakarta Data для работы с реляционными БД, но, в теории, Jakarta Data абстрагирует любое хранилище.

Поскольку Jakarta Data призвана работать с потенциально любым хранилищем, то она не может делать предположение о том наборе операций, которое хранилище может выполнять. Например, в рамках спецификации языков запросов JPQL и HQL описан JOIN clause, что логично, ведь как JPQL, так и HQL представляют собой язык запросов для реляционной БД, в которой понятие JOIN определено и имеет смысл. Однако, в Apache Cassandra или Redis понятие JOIN как таковое отсутствует. Поэтому Jakarta Data не может в своей спецификации JDQL определить такие вещи, как JOIN или Common Table Expressions и т. п.

Сам JDQL довольно ограниченный язык, лексическая структура которого гораздо проще, чем у потенциального JPQL и тем более HQL. На данный момент можно осторожно сказать, что операции, предоставляемые JDQL, являются неким подмножеством операций, предоставляемых JPQL, а те в свою очередь являются подмножеством HQL.

Некоторые подробности спецификации репозиториев Jakarta Data

Есть одна важная особенность в работе репозиториев, которую мы не обсудили. Рассмотрим код, который написан с использованием Spring Data JPA:

В Spring Data с использованием JPA всё достаточно просто. Разработчик, написавший код выше, хотел добавить к посту комментарий. Цепочка действий была такая:

  • Находим пост по ID.

  • Ищем уже существующий к этому посту комментарий от этого автора (предположим, что нельзя оставить более одного комментария).

  • Если комментарий существует — генерируется исключение. Если же нет, то добавляем новый комментарий.

В описанном выше случае код работает благодаря механизму dirty checking:

  • Когда открывается транзакция, Spring создаетEntityManager и привязывает его к ней.

  • Когда выполняется запрос — например, поиск поста по идентификатору через HQL, — этот пост попадает в persistence-контекст.

  • После изменения сущности — например, при вызове метода add() или set() — она становится «грязной». Когда в конце транзакции происходит flush, Hibernate замечает, что сущность «грязная» и сбрасывает изменения, которые были сделаны в рамках текущей транзакции с этой сущностью в БД.

Flush вызывается или вручную разработчиком, или автоматически, как в нашем случае, когда persistence-контекст закрывается при завершении транзакции.

Обращаю ваше внимание, друзья, что dirty checking работает только благодаря persistence-контексту — временному хранилищу, где Hibernate отслеживает все изменения сущностей, кеширует их и т. д.

Теперь рассмотрим аналогичный код на Hibernate, где PostRepository — это репозиторий уже не Spring Data, а Jakarta Data (его сгенерирует Hibernate APT во время сборки), и где используется @Transactional из JTA, а не из Spring Framework.

Казалось бы, код выглядит точно так же, и даже под капотом тот же Hibernate, но если попытаться достать ленивую коллекцию (например, комментарии из поста), то появится ошибка Lazy Initialization Exception.

И здесь мы приходим к новой проблеме.

Дело в том, что понятие persistence context специфично для Hibernate и для JPA. Его наличие логично, потому что persistence context как кеш для транзакции хорошо укладывается в концепцию MVCC реляционных баз данных, особенно если мы работаем с REPEATABLE READ. Но Jakarta Data, опять же, была спроектирована для работы потенциально с любым хранилищем.

Поэтому, чтобы не обременять реализации и не делать лишних предположений, Jakarta Data декларирует, что её репозитории stateless по-умолчанию.

Что означает «stateless»? По сути это означает, что репозиторий работает по принципу «сделали запрос, получили данные, и всё». На этом точка. Репозиторный слой больше не хранит в себе никакой информации или состояния для этой сущности. То есть, отсутствует Persistence Context.

Возникает тогда другой вопрос «А как же тогда Hibernate умудряется генерировать реализации репозиториев без persistence context?» Суть JPA заключается в EntityManager и в том контексте, которым этотEntityManager управляет. Всё так, но это же абстракции из спецификации JPA, а Hibernate — это нечто большее, и у него есть свой ответ на это: StatelessSession. Репозитории, которые генерирует Hibernate на этапе сборки не используют EntityManager или Session, они используют StatelessSession:

StatelessSession существует в Hibernate уже долгие годы, однако используют этот API довольно мало. В нашем примере stateless-сессия означает, что у репозиториев, сгенерированных Hibernate:

  • отсутствует persistence context;

  • соответственно, отсутствует огромный пласт функциональности, такой как lazy loading, каскадирование или dirty checking.

Это и привело к той ошибке, что мы увидели в примере выше.

При этом StatelessSession даёт определенные преимущества:

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

  • Предотвращает нежелательные запросы в базу данных. На факт того, что lazy loading и dirty checking не работает также можно смотреть как на преимущество с точки зрения прозрачности работы ORM-фреймворка.

Казалось бы, чем StatelessSession Spring Data не угодила. А вот в чём дело.

Spring Data JPA не просто так имеет постфикс JPA — она построена не поверх Hibernate, а поверх стандарта JPA в целом. Иными словами, предполагается, что вы можете работать через Spring Data JPA не только поверх Hibernate, но и поверх Eclipse Link, например. На самом деле, так лучше не делать по ряду причин, но тем не менее.

Так вот, StatelessSession не является частью стандарта JPA (не является пока что, есть планы её туда внести), несмотря на то, что она давно есть в Hibernate. Поэтому Spring Data JPA, очевидный кандидат на реализацию Jakarta Data, проектировали вокруг всем нам известной EntityManager/Session и stateful-контекста. Код, который уже написан (вспоминаем пример с Post), будет просто сломан, если Spring Data JPA адаптирует Jakarta Data как есть.

Особенности работы с QueryByMethodName

Давайте пойдём дальше. В конце концов, Spring Data не ограничивается только Spring Data JPA. Возможно, можно будет использовать какой-то другой модуль Spring Data для работы с Jakarta Data, например, Spring Data JDBC.

Тут есть другой нюанс. Все мы знаем, что в Spring Data можно формировать запросы просто по имени метода. Это упрощает процесс разработки и, что крайне важно, формирует единообразный интерфейс для работы с данными для всех модулей Spring Data .

Например, метод findByTitleContaining Spring Data JDBC трансформирует в примерно такой SQL:

SELECT * FROM books WHERE title LIKE ‘%’ || ? || ‘%’

Для таких простых запросов этот подход работает идеально. Но когда требуется более сложная логика — например, запросы с диапазоном дат или игнорированием регистра, — то название метода может стать довольно сложным и тяжелочитаемым.

Как мы уже увидели ранее, Jakarta Data в целом отказалась от такого подхода. Авторы решили, что вы, как разработчик, чтобы подсказать реализации, какой именно запрос надо сгенерировать, будете использовать маркерные аннотации, вроде @Insert, @Delete и @Find. Иными словами, имя метода в Jakarta Data по-умолчанию никак не влияет на сгенерированный реализацией запрос.

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

С таким основным подходом, который выбрала Jakarta Data, есть небольшая проблема: с помощью аннотаций не очень понятно, как выражать более сложные условия, такие как, например, BETWEEN.

Для таких случаев Jakarta Data предлагает использовать JDQL. Я уже говорил про этот язык запросов, можете взглянуть на примере ниже:

Ничего сложного, но это заставляет реализацию писать свой механизм парсинга JDQL-запросов. В теории, Spring Data JDBC, например, могла это сделать, но поддержка дополнительного языка и дополнительной грамматики довольно нетривиальна, особенно, когда спецификацию разрабатываете не вы.

Дополнение. Поддержка Spring с Jakarta Data

Небольшое дополнение для Хабра. На момент доклада не было известно, но уже озвучили: Gaving King и команда Hibernate пишет поддержку использования Spring в репозиториях Jakarta Data. Пока это лишь новость, официального превью, сроков или ga-релиза нет. В рамках сообщества Spring Айо эту тему уже обсуждали.

Заключение

Jakarta Data, являясь относительно новой спецификацией в проекте Jakarta EE, в основном разрабатывается людьми из Red Hat.

Существующие модули Spring Data не могут реализовать спецификацию Jakarta Data, потому что есть существенные различия в модели репозиториев, используемых абстракциях (Session vs StatelessSession), способах инференса запросов из методов (аннотации vs имя метода) и т. д.

Писать новый модуль Spring Data, который реализовал бы спецификацию Jakarta Data, разрабатываемую конкурирующей с вами экосистемой, особо смысла не имеет. Поэтому, на данный момент, Jakarta Data, скорее всего, будет использоваться, в основном, с проектами, которые написаны на Quarkus, представляя собой альтернативу ORM Panache и Active Record.

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


  1. ris58h
    14.08.2025 16:04

    Для демонстрации примеров кода есть отдельный блок форматирования. Не надо код картинками вставлять.


  1. stas_dubich
    14.08.2025 16:04

    Вы забыли уточнить, все это хорошо работает пока проект уровня hello world, как только дело доходит до чего то более сложного - привет нативные библиотеки и нативное написание запросов

    Можно конечно попробовать натянуть сову на глобус, попробовать все эти JPQL и прочее, но в конечном итоге придет понимание что лучше натива - ничего нет

    Опять же повторюсь, речь про сложные проекты, для CRUD - все эти хибернейты, jpa идеальны


    1. gsaw
      14.08.2025 16:04

      Статья же про Jakarta Data и почему нет ее реализации в спринге. Или надо добавлять предупреждение в каждой статье? "JPQL вредит вашему здоровью".

      "придет понимание что лучше натива - ничего нет "

      Максималистское утверждение из разряда, "лучше ассемблера ничего нет". Все хорошо в меру и ничто не мешает в проекте смешивать нативные запросы и JPQL.


      1. stas_dubich
        14.08.2025 16:04

        При чем тут максимализм ?) Я же написал что для CRUD - ок


      1. IoannGolovko
        14.08.2025 16:04

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


        1. stas_dubich
          14.08.2025 16:04

          Именно поэтому все эти извращения аля удобные фреймворки для sql - не используем)

          На дистанции важнее производительность и предсказуемость, чем псевдоскорость разработки

          Я уж молчу про те километровые sql которые хибернейт генерирует, а чего только стоит n+1, а на выходе получаем сайты и личные кабинеты которые по 5-25 секунд прогружаются ))


    1. artptr86
      14.08.2025 16:04

      А что делать в случае коробочных продуктов (типа JIRA), которые могут использовать разные базы данных?


    1. agoncharov
      14.08.2025 16:04

      Сложные проекты на 80 процентов состоят из CRUD-а, поэтому хибернейт и jpa актуальны почти везде


  1. vlad4kr7
    14.08.2025 16:04

    JPA подразумевается JPA 2 или JPA 1 ?


    1. aleksandy
      14.08.2025 16:04

      JPA 3.2+, скорее всего