В Hibernate 7.4 появилось несколько улучшений упрощающих загрузку страницы данных вместе со связанными дочерними коллекциями, доступ к историческим данным и ведение журнала аудита.

В статье рассматриваются следующие возможности:

  • Limits и Fetch Joins: как Hibernate 7.4 улучшает работу с постраничными запросами, включающими загрузку ассоциаций.

  • History и Audit Tables: как новые возможности поддерживают запросы к состоянию сущности в разные моменты времени и работу с историческими данными.


Пример кода к этой статье доступен в репозитории GitHub.

Limits и Fetch Joins

Одна из типичных задач в приложениях, работающих с данными, — загрузить страницу родительских сущностей вместе со связанной коллекцией дочерних сущностей. Например, предположим, что в приложении есть сущность Order с коллекцией Set<OrderItem> , и нам нужно загрузить несколько первых заказов вместе с их позициями.

List<Order> orders = session
        .createSelectionQuery(
            "select o from Order o join fetch o.items order by o.id",
            Order.class
        )
        .setMaxResults(10)
        .getResultList();

В версиях Hibernate до 7.4 применение ограничения к запросу с fetch join коллекции нельзя было безопасно передать на уровень базы данных. Поскольку каждый Order может содержать несколько строк OrderItem , прямое ограничение SQL-результата могло обрезать коллекцию позиций заказа. Чтобы избежать возврата неполных коллекций, Hibernate загружал все подходящие строки из базы данных и применял пагинацию в памяти на уровне приложения.

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

До Hibernate 7.4 генерируемый SQL выглядел примерно так:

select
    o1_0.id, i1_0.order_id, i1_0.id, i1_0.product_code,
    i1_0.quantity, o1_0.order_number, o1_0.status
from
    orders o1_0
        join
    order_items i1_0
    on o1_0.id=i1_0.order_id

Как видно, ограничение (пагинация) не применяется на уровне SQL-запроса. Поэтому будут загружены все orders и связанные с ними order_items, что может оказаться очень затратной операцией и привести к OutOfMemoryException.

Hibernate при этом записывает в лог предупреждение следующего вида:

[WARN] HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory

Один из способов запретить Hibernate выполнять пагинацию в памяти — задать следующее свойство:

hibernate.query.fail_on_pagination_over_collection_fetch=true

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

Hibernate 7.4 решает эту проблему с помощью вложенных запросов. Вместо того чтобы применять ограничение непосредственно к результату JOIN, Hibernate сначала определяет ограниченный набор идентификаторов родительских сущностей, а затем загружает связанную коллекцию только для этих строк.

Это позволяет выполнять пагинацию на стороне базы данных, при этом для каждого выбранного Order возвращаются полные коллекции позиций.

В Hibernate 7.4 SQL генерируется следующим образом:

select
        o1_0.id, i1_0.order_id, i1_0.id, i1_0.product_code,
        i1_0.quantity, o1_0.order_number,o1_0.status 
    from
        (select
            o1_0.id, o1_0.order_number, o1_0.status 
        from
            orders o1_0 
        where
            exists(select
                1 from order_items i1_0 
            where
                o1_0.id=i1_0.order_id) 
        offset
            ? rows 
        fetch
            first ? rows only) o1_0(id, order_number, status) 
    join
        order_items i1_0 
            on o1_0.id=i1_0.order_id

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

History и Audit Tables

Hibernate 7.4 добавляет встроенную поддержку history tables и audit tables. Обе возможности позволяют отслеживать изменения данных сущностей, но решают немного разные задачи: history tables позволяют запрашивать состояние сущности в определённый момент времени, тогда как audit tables фиксируют последовательность и природу изменений, произошедших с сущностью.

Рассмотрим следующую сущность Product :

@Entity
@Table(name = "products")
class Product {
    //fields id, code, name, price
}

History Tables

Чтобы включить историю (т.е. history table) для Product, аннотируйте сущность с помощью @Temporal и при необходимости укажите имя таблицы истории с помощью @Temporal.HistoryTable.

@Entity
@Table(name = "products")
@Temporal
@Temporal.HistoryTable(name="products_history")
class Product {
    //fields id, code, name, price
}

При таком маппинге Hibernate сохраняет предыдущие версии строк продукта в таблице products_history . Таблица включает колонки сущности и две дополнительные колонки: effective, которая фиксирует момент активации версии, и superseded, которая фиксирует момент её замены.

products_history :

id

code

name

price

effective

superseded

2251

P1000

Product-1000

40.00

2026-05-15 08:21:39.949001 +00:00

null

2301

P1001

Product-1001

90.00

2026-05-15 08:22:24.765883 +00:00

2026-05-15 08:22:24.778067 +00:00

2301

P1001

Product-1001

100.00

2026-05-15 08:22:24.778067 +00:00

null

Данные сущности Product в заданный момент времени можно получить следующим образом:

Instant someTime = ...
try (var session = sessionFactory.withOptions().asOf(someTime).open()) {
    var product = session.find(Product.class, productId);
    
}

Благодаря этому запросы состояния сущности в момент времени ‘t’ выглядят как обычный поиск сущностей, тогда как Hibernate самостоятельно разрешает нужную историческую строку за кулисами.

Hibernate предлагает несколько стратегий маппинга временны́х сущностей: NATIVE, SINGLE_TABLE и HISTORY_TABLE. Подробнее см. в разделе Temporal data.

Таблицы аудита

Раньше приложения на базе Hibernate, как правило, использовали отдельную библиотеку Hibernate Envers для аудита изменений сущностей. Hibernate 7.4 встраивает поддержку таблиц аудита непосредственно в Hibernate ORM, так что приложения могут использовать эту функциональность нативно — без подключения Envers для данного сценария.

Поддержка аудита включается добавлением @Audited и может быть привязана к произвольной таблице с помощью @Audited.Table.

@Entity
@Table(name = "products")
@Audited
@Audited.Table(name="products_aud_log")
class Product {
    //fields id, code, name, price
}

При включённом аудите Hibernate записывает в таблицу аудита по одной строке на каждое изменение. В отличие от таблицы истории, таблица аудита ориентирована на фиксацию того, какая именно операция произошла (DELETE, UPDATE или INSERT) и когда именно.

id

code

name

price

rev

revtype

2001

P1002

Product-1002

90.00

2026-05-13 14:58:17.505775 +00:00

0

2001

P1002

Product-1002

100.00

2026-05-13 14:58:17.518194 +00:00

1

Значения rev — временны́е метки момента изменения. Значения revtype представлены с помощью ModificationType enum следующим образом:

public enum ModificationType {
    /**
    * Creation, encoded as 0
    */
    ADD,
    /**
    * Modification, encoded as 1
    */
    MOD,
    /**
    * Deletion, encoded as 2
    */
    DEL
}

Подробнее см. в разделе Audit logs.

Итоги

В большинстве приложений для отображения списков ресурсов используется пагинация, и раньше приходилось писать собственную логику для постраничной загрузки данных вместе со связанными дочерними коллекциями. Теперь это решается на уровне самого фреймворка. Кроме того, для реализации аудита мы привыкли опираться на внешние библиотеки, такие как Envers — теперь же эта возможность предоставляется самим Hibernate.

Hibernate 7.4 привносит практичные улучшения, решающие реальные задачи в приложениях на базе JPA/Hibernate. Будь то оптимизация поведения запросов при пагинации или отслеживание исторических данных — Hibernate 7.4 сокращает объём инфраструктурного кода, который нужно писать самостоятельно, и обеспечивает более богатую поддержку из коробки без необходимости подключать дополнительные библиотеки.

Изучите эти новые возможности на примере данного репозитория на GitHub.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

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