JavaRush /Курсы /Hibernate deep-dive /Ordering inserts/updates в Hibernate

Ordering inserts/updates в Hibernate

Hibernate deep-dive
23 уровень , 3 лекция
Открыта

1. Batching включён, но запросы одиночные

IDENTITY уже не мешает вставкам, flush()/clear() держат под контролем размер контекста — и тут всплывает ещё одна вещь: batch может почти не собраться, если flush выдаёт рваный поток SQL. То есть проблема уже не в самом факте массовой записи, а в том, насколько однотипными доходят операции до JDBC.

Представьте, что вы включили hibernate.jdbc.batch_size, сделали всё «как в примерах», запускаете импорт — и видите в логах всё те же одиночные INSERT/UPDATE, как будто batching вообще не пришёл на работу. Это очень частая ситуация: batch size задаёт возможность, но реальное поведение зависит от того, насколько ваш поток SQL похож на «серийное производство», а не на «хаотичный склад, где всё вперемешку».

Ключевой момент здесь в том, что JDBC batching работает вокруг PreparedStatement. Условно, драйвер умеет сказать: «Окей, у меня есть один и тот же SQL вида insert into product (...) values (?, ?, ?) — давайте я выполню его 50 раз одной пачкой». Но если ваш flush выдаёт последовательность INSERT product, потом INSERT category, потом снова INSERT product, потом INSERT category, то с точки зрения драйвера это уже два разных PreparedStatement, между которыми он вынужден постоянно переключаться. В результате пачки либо получаются маленькими (например, по 1–2 операции), либо вообще «схлопываются» до одиночных выполнений.

И вот здесь появляется тема сегодняшней лекции: hibernate.order_inserts и hibernate.order_updates. Это не «ещё два магических флажка ускорения», а попытка Hibernate переупорядочить накопленные операции записи так, чтобы рядом стояли похожие SQL-операции. То есть Hibernate пытается сделать для вашего write-потока то, что вы бы сделали сами, если бы были очень терпеливым человеком: сложить все одинаковые бумажки в одну стопку, а потом аккуратно отнести их в БД.

Чтобы не было путаницы: это никак не связано с ORDER BY в SELECT. Мы не сортируем данные, которые читаем. Мы сортируем операции записи (INSERT/UPDATE) перед тем, как отправить их в базу во время flush.

2. Flush и ActionQueue

Если пытаться представить Hibernate как «человека», то до flush он больше похож на бухгалтера: он записывает в свою тетрадку, что вы создали сущность, поменяли поле, удалили элемент коллекции, но не бежит каждый раз в базу с криком «СРОЧНО UPDATE!!!». А вот в момент flush он превращается в курьера: достаёт тетрадку и начинает реально отправлять SQL в БД. И именно в этот момент можно что-то «упорядочить».

Технически внутри Hibernate есть структура, которую обычно описывают как очередь/набор очередей действий — что-то вроде «плана работ на flush». В ней отдельно копятся insert-операции, update-операции, удаления, операции по коллекциям и т.д. На лекциях про flush мы уже обсуждали, что Hibernate выполняет этот план не в произвольном порядке, а в определённой логике, чтобы не ломать внешние ключи и целостность.

hibernate.order_inserts и hibernate.order_updates вмешиваются не в вашу бизнес-логику и не в dirty checking, а в то, как именно будет упорядочен этот план действий. То есть ваш Java-код остаётся тем же: вы вызываете persist(), меняете поля у managed-сущностей, работает каскад — всё как раньше. Меняется только порядок, в котором Hibernate будет отправлять SQL во время flush.

Полезно держать в голове такую схему (упрощённую, но рабочую):

flowchart TD
    %% До flush Hibernate только "копит" действия, SQL обычно ещё не улетает в базу
    A["Вы вызываете persist()/меняете managed-entity"] --> B["Hibernate копит действия в очереди flush"]
    %% Во время flush Hibernate формирует и выполняет план записи в БД
    B --> C["Наступает flush: AUTO или вручную"]
    C --> D{"Включены order_inserts / order_updates?"}
    D -- нет --> E["Выполняем действия в естественном порядке очередей"]
    D -- да --> F["Сортируем insert/update/collection actions для лучшей группировки"]
    E --> G["JDBC driver выполняет SQL"]
    F --> G["JDBC driver выполняет SQL (больше шансов на batch)"]

Важное следствие: ordering работает внутри конкретного flush. Если вы делаете flush слишком часто, у Hibernate просто не накапливается достаточная «куча» действий, которую было бы выгодно сортировать. И наоборот: если вы копите 100 000 действий в одном flush, вы получите и потенциал batching, и потенциальную боль по памяти/времени сортировки. Поэтому ordering — это усилитель аккуратно построенного write-flow, а не волшебная замена flush()/clear().

3. hibernate.order_inserts: группируем INSERT

Когда вы включаете hibernate.order_inserts, Hibernate начинает упорядочивать insert-операции по типу сущности и значению первичного ключа, чтобы JDBC batching работал эффективнее. По умолчанию значение этого свойства false. В документации важна ещё одна фраза: порядок сортировки учитывает зависимости внешних ключей, то есть Hibernate не будет «ради красоты» вставлять детей раньше родителей и провоцировать FK-ошибки.

Польза для batching

У batching есть довольно скучная, но честная механика: драйвер бьёт batching по границам PreparedStatement. Для Product и Category SQL будет разный, значит PreparedStatement разные. Если вы вперемешку вставляете продукты и категории, то вы как будто заставляете водителя маршрутки каждые 10 метров менять автобус на трамвай и обратно — формально движение есть, но что-то тут явно не оптимально.

order_inserts делает проще: «Давай сначала все продукты, потом все категории (но не нарушая FK-зависимости)». В результате PreparedStatement для product может накопить пачку из, например, 20 вставок, а потом подготовится пачка для category.

Мини-сценарий в нашем Commerce Persistence Lab

Возьмём предельно житейский кусок кода, который вполне может появиться в лабораторном импорте каталога. Мы чередуем persist() разных сущностей, потому что входные данные пришли «парами», и рука сама написала один цикл:

import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class CatalogMixedImportService {

    private final EntityManager em;

    public CatalogMixedImportService(EntityManager em) {
        this.em = em; // EntityManager нужен, чтобы persist() складывал действия в persistence context
    }

    @Transactional
    public void importMixed(List<Product> products, List<Category> categories) {
        // Важно: предполагаем, что списки синхронизированы по размеру и соответствию элементов
        for (int i = 0; i < products.size(); i++) {
            // На уровне кода это выглядит как "две persist подряд"
            em.persist(products.get(i));   // будет INSERT в таблицу product (во время flush)
            em.persist(categories.get(i)); // будет INSERT в таблицу category (во время flush)
            // Из-за такого чередования без ordering SQL часто получается "рваным", и batching недобирается
        }
        // SQL фактически полетит в БД не здесь, а на flush/commit (если не было принудительного flush ранее)
    }
}

На уровне Java это выглядит как «две одинаковые операции в цикле». Но на уровне SQL Hibernate увидит две разные таблицы, и без ordering поток может стать «рваным»: insert product, insert category, insert product, insert category… И batch по каждой таблице может не успевать набраться до вашего batch_size.

С включённым hibernate.order_inserts Hibernate получает шанс во время flush переупорядочить insert-действия так, чтобы они шли группами по типам сущностей (с оглядкой на зависимости).

SQL-след (упрощённо)

Пусть ваша БД видит примерно такие запросы (для иллюстрации):

Без упорядочивания:

-- Запросы чередуются: драйверу приходится прыгать между разными PreparedStatement
insert into product (sku, name) values (?, ?);
insert into category (code, name) values (?, ?);
insert into product (sku, name) values (?, ?);
insert into category (code, name) values (?, ?);

С hibernate.order_inserts=true у Hibernate появляется шанс превратить это во что-то ближе к:

-- Запросы сгруппированы по таблице: выше шанс, что это реально уйдёт одной пачкой (batch)
insert into product (sku, name) values (?, ?);
insert into product (sku, name) values (?, ?);
insert into category (code, name) values (?, ?);
insert into category (code, name) values (?, ?);

И вот тут batching начинает выглядеть как batching: два одинаковых insert into product ... можно реально отправить пачкой, а потом два одинаковых insert into category ... — другой пачкой.

Важный «психологический» момент

Когда ordering включён, вы можете увидеть в логах, что SQL идёт не «как в цикле». Это нормально. Hibernate не обязан сохранять тот порядок SQL, который вам психологически приятен, он обязан сохранять корректность данных. А порядок SQL — это уже часть стратегии выполнения, и она как раз настраивается этими флагами.

Если вам нужно, чтобы SQL шёл строго как в коде (обычно это нужно только для специфических триггеров или отладочных экспериментов), ordering лучше не включать. В нормальном же backend-потоке опираться на порядок DML — не самая надёжная идея, потому что flush сам по себе уже может произойти раньше, чем «вы ожидали» (например, перед запросом).

4. hibernate.order_updates: группируем UPDATE и коллекции

hibernate.order_updates по смыслу очень похож на order_inserts, но он работает на update-потоке и чуть шире по охвату. По умолчанию это свойство тоже выключено (false). Когда оно включено, Hibernate упорядочивает update-операции по типу сущности и первичному ключу, а также упорядочивает операции, связанные с изменением коллекций, по роли коллекции и значению внешнего ключа — опять же, ради более эффективного JDBC batching.

В документации есть ещё один тонкий нюанс: такое упорядочивание также снижает шанс unique key violation, когда элемент коллекции «переезжает» от одного родителя к другому, потому что Hibernate старается выполнять операции удаления/«removal» до операций, которые removal не содержат. Это не «железная гарантия», но полезная защита от очень неприятных редких конфликтов.

Рваный update-поток

С insert-потоком обычно всё понятно: вы создаёте новые объекты. А update-поток часто появляется «сам»: вы загрузили набор сущностей, поменяли поля, Hibernate сделал dirty checking — и в flush накопилась куча UPDATE.

Если вы в одной транзакции меняете сразу несколько типов сущностей (например, товары и остатки, или заказы и позиции), то update-запросы могут чередоваться, особенно если изменения происходят в разных участках кода, через каскады, хелперы и прочую «жизнь проекта».

order_updates пытается разложить эти изменения в «аккуратные стопки»: сначала все обновления одного типа, потом другого. Это повышает шанс, что JDBC batching сможет реально собрать пачки по PreparedStatement.

Небольшой пример из домена: обновляем товары и остатки в одной транзакции

Представим сервисный сценарий, который по какой-то причине обновляет и Product, и InventoryItem в одной транзакции. Это не обязательно «правильно» с точки зрения домена, но в лабораторном проекте такие штуки нужны, чтобы видеть поведение Hibernate:

import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;

@Service
public class ReindexService {

    private final EntityManager em;

    public ReindexService(EntityManager em) {
        this.em = em; // Нужен, чтобы сущности были managed в рамках транзакции
    }

    @Transactional
    public void touchProductAndInventory(Product product, InventoryItem item) {
        // Важно: product и item должны быть managed (загружены в этой сессии или merge'нуты),
        // иначе Hibernate может не сделать ожидаемый UPDATE на flush.
        product.setName(product.getName() + " *");   // будет UPDATE product (dirty checking на flush)
        item.setUpdatedAt(Instant.now());            // будет UPDATE inventory_item (dirty checking на flush)

        // Если таких пар много, без order_updates UPDATE'ы часто будут прыгать между таблицами
        // и batching будет хуже работать.
    }
}

Если такой метод вызывается в цикле для многих пар product+inventory, то без order_updates flush может легко получить последовательность обновлений, где SQL будет прыгать между таблицами, что ухудшает batching. А hibernate.order_updates=true даёт шанс сгруппировать update-запросы по типу сущности и PK.

Коллекции и роль коллекции

Фраза «роль коллекции» звучит как что-то из RPG («у вашей коллекции роль “маг” или “танк”?»), но в Hibernate это намного прозаичнее. Роль коллекции — это по сути «какая именно коллекция в какой сущности»: например, PurchaseOrder.items или Customer.addresses. Даже если элементы коллекции — это отдельные entity, вокруг изменения коллекции Hibernate всё равно выполняет набор действий: добавить элемент, удалить элемент, поменять связь, иногда обновить FK-колонку.

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

Ordering updates и риск deadlock

В прошлых модулях (про locking) мы обсуждали, что deadlock’и часто возникают, когда две транзакции берут блокировки на строки в разном порядке: одна обновляет id=10 потом id=20, другая — наоборот. Если update’ы упорядочены по PK, обе транзакции чаще будут обновлять строки в одинаковом порядке, и риск взаимной блокировки становится ниже.

Но здесь важно не впасть в «магическое мышление». hibernate.order_updates — это тюнинг write-потока ради batching и чуть более детерминированной последовательности. Он не заменяет корректные transaction boundaries, не лечит неправильные lock-стратегии и не превращает конкурентность в сказку со счастливым концом.

5. Включаем ordering в Spring Boot 4

На практике здесь нужны всего две Hibernate-property рядом с уже включённым batching. Если hibernate.jdbc.batch_size всё ещё равен 0, ordering нечего усиливать: сортировать можно сколько угодно, batch от этого сам не появится.

spring:
  jpa:
    properties:
      hibernate.order_inserts: true
      hibernate.order_updates: true

Если batch size задаёте локально на Session, эти два флага всё равно остаются полезными: они влияют на порядок действий внутри flush, а не на то, откуда взялся размер партии.

order_inserts и order_updates не ускоряют SQL в вакууме. Они лишь делают write-поток более однородным внутри flush, чтобы JDBC batching чаще собирал полноценные пачки по одинаковым PreparedStatement.

Если ваш insert/update поток и так однотипный, разницы может почти не быть. Но в mixed write-flow эти две строки часто дают тот самый эффект “batching вроде включён, а теперь наконец начал выглядеть как batching”.

6. Когда ordering заметен, а когда нет

Обычно такие флаги хочется включить «на всякий случай». Это человеческое желание, я не осуждаю — я сам так делал, потом сам же разгребал последствия и писал себе в голове баг-репорт. Поэтому полезно заранее понимать, где order_inserts/order_updates дают видимый профит, а где вы, скорее всего, потратите время на настройку и не увидите разницы.

Наблюдение простое: ordering особенно заметен, когда внутри одного flush у вас есть много операций записи разных типов, и эти операции изначально попадают в очередь «перемешанными». Это бывает из-за чередования persist() разных сущностей, из-за каскадов, из-за сложного графа изменений, из-за того, что код «размазан» по нескольким методам и не держит в голове подготовку батча.

Если же у вас сценарий вида «берём 5000 Product и обновляем им одно поле в цикле», то у вас и так получится серия однотипных UPDATE product ..., и ordering может ничего не добавить: там уже нечего улучшать, поток и так однородный.

Удобно зафиксировать это в маленькой табличке (без религии, просто как инженерный ориентир):

Сценарий записи в рамках одного flush Что происходит без ordering Что может улучшиться с ordering
Массовые INSERT одного типа (Product только) SQL уже однородный Обычно почти без разницы
Чередование разных типов (Product, Category, Assignment) SQL прыгает между таблицами Больше шансов собрать полноценные батчи по типам
Массовые UPDATE по разным типам в одной транзакции update-поток перемешан Группировка UPDATE по типу и PK, лучше batching
Очень частый flush() (например, каждую итерацию) batching и ordering не успевают «развернуться» Эффект минимален, потому что нечего сортировать
GenerationType.IDENTITY на сущностях с insert-heavy сценарием Hibernate не может нормально батчить insert ordering не спасает принципиальную проблему

И ещё один важный нюанс: ordering — это дополнительная работа (сортировка, перестройка очередей). Если вы включаете эти флаги в сценарии, где batching почти невозможен или не нужен, вы можете получить «настроили, стало даже чуть медленнее». И это не баг Hibernate, это честный результат: вы добавили вычисления, а выиграть было негде.

7. Типичные ошибки

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

Ошибка №1: включить ordering, но забыть, что batching выключен.
Иногда в конфиге появляется hibernate.order_inserts=true, а hibernate.jdbc.batch_size остаётся 0. В результате Hibernate честно сортирует insert/update действия, но JDBC batching как механизм не включён, и вы не увидите обещанного эффекта. Более того, вы добавили сортировку, но не получили отдачу. Напоминание очень простое: batching включается только при положительном hibernate.jdbc.batch_size.

Ошибка №2: ожидать, что ordering исправит проблему IDENTITY.
Если стратегия генерации id делает batch insert принципиально невозможным (как это бывает с IDENTITY в Hibernate), то order_inserts не станет «обходным путём». Он не превращает невозможное в возможное. В таких сценариях вы сначала решаете проблему стратегии идентификатора и формы insert-flow, и только потом рассматриваете ordering как тюнинг.

Ошибка №3: делать flush слишком часто и удивляться, что ordering “не работает”.
Ordering работает внутри flush, а значит ему нужно, чтобы flush имел достаточно накопленных действий. Если вы вызываете flush() на каждой итерации (или flush постоянно срабатывает автоматически из-за ваших же запросов внутри транзакции), то сортировать просто нечего: в очереди слишком мало операций, чтобы группировка дала разницу.

Ошибка №4: путать hibernate.order_inserts/hibernate.order_updates с ORDER BY и настройками чтения.
Очень частая путаница: кто-то слышит слово «order» и начинает искать, как это влияет на сортировку списка товаров в админке. Никак. Это настройка порядка DML во время flush. Для чтения у вас другой инструментарий: запросы, JOIN FETCH, EntityGraph, projections и так далее — но это вообще другая часть курса и другая “ось мира”.

Ошибка №5: рассчитывать на конкретный порядок SQL как на часть бизнес-логики.
С включённым ordering SQL может перестать идти в том порядке, в котором вы делали persist() или меняли поля. Но даже без ordering порядок DML во время flush может зависеть от каскадов, зависимостей и внутренних правил Hibernate. Если у вас есть бизнес-требование “в БД сначала обязательно должна появиться запись X, потом Y” — это обычно сигнал, что вы смешали бизнес-правила с инфраструктурной деталью. Hibernate гарантирует корректность, а не «приятный глазу» порядок логов.

1
Задача
Hibernate deep-dive, 23 уровень, 3 лекция
Недоступна
`hibernate.order_inserts` для смешанного потока вставок
`hibernate.order_inserts` для смешанного потока вставок
1
Задача
Hibernate deep-dive, 23 уровень, 3 лекция
Недоступна
`hibernate.order_updates` для серии обновлений
`hibernate.order_updates` для серии обновлений
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ