Выбор bulk, entity-loop и StatelessSession

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

1. Массовые операции: один размер — не один подход

Когда вы впервые сталкиваетесь с задачей «обновить 50 000 строк», мозг начинает искать один универсальный рецепт. Хочется верить, что где-то есть волшебная кнопка “Make it fast”, и она называется либо “batching”, либо “bulk”, либо “какой-то хитрый режим Hibernate”. Реальность прозаичнее: разные сценарии требуют разной семантики изменения данных, а уже потом — разной техники исполнения.

К этому месту у нас уже есть три разных режима: managed entity-loop, bulk mutation query и StatelessSession. Поэтому дальше важнее не заново разбирать механику каждого, а научиться быстро выбирать подход под смысл операции, объём данных и цену persistence context.

Представьте, что вам нужно поменять объявления на подъездах в большом районе. Иногда вам действительно нужно зайти в каждый подъезд, посмотреть, что там висит, и аккуратно заменить только то, что соответствует условию — это entity-loop. Иногда вам нужно просто поменять шаблон на сервере, и все объявления в электронных табло обновятся одним махом — это bulk update. А иногда вы хотите пройтись по подъездам, но без «журнала посещений» и без того, чтобы хранить в голове историю всех подъездов, где вы уже были — это как раз StatelessSession.

В нашем проекте Commerce Persistence Lab эти ситуации выглядят очень жизненно: товары (Product) иногда надо массово скрыть или «переоценить», а исторические снапшоты остатков (InventorySnapshot) периодически нужно чистить, чтобы таблица не превращалась в музей бесконечного прошлого. И вот здесь важно не начать лечить любую проблему одним инструментом «потому что он звучит круче».

2. Таблица: entity-loop, bulk и StatelessSession

Чтобы голова не превращала выбор в философский спор, полезно один раз посмотреть на эти инструменты как на три разных договора с Hibernate. В одном договоре Hibernate берёт на себя много обязанностей и берёт за это «налог» ресурсами. В другом — вы говорите “я сам”, а Hibernate просто выполняет SQL. В третьем — вы тоже говорите “я сам”, но всё ещё остаетесь внутри Hibernate API, просто без persistence context.

Ниже — «шпаргалка на стену» (или на второй монитор, рядом с SQL-логом, чтобы вы чувствовали себя взрослым разработчиком, даже если кофе остыл):

Подход Что реально происходит Когда это естественно Главная цена/риск Что делать с persistence context
entity-loop (обычный
EntityManager
/
Session
)
Загружаем managed-сущности → меняем поля → dirty checking →
flush
Логика зависит от каждой строки, нужны доменные проверки, helper-методы, каскады, привычный lifecycle Много объектов в памяти, snapshots, дорогой dirty checking, риск «забыли clear в большом цикле» Следить за размером контекста, в больших циклах думать про
flush/clear
bulk
update
/
delete
(mutation query)
Один запрос меняет/удаляет строки в БД без загрузки сущностей «Одно правило на всех» по условию, технические массовые правки, cleanup Stale persistence context: в памяти останутся старые значения; также bypass managed-модели Обычно нужен явный протокол:
flush
(если были изменения) → bulk →
clear
или
refresh
→ повторное чтение
StatelessSession Нет first-level cache, нет automatic dirty checking; операции вызываются явно Нужна построчная обработка, но не нужен managed-граф; хочется избежать раздувания persistence context Можно легко «ничего не сохранить», если забыть
update
/
delete
; нет каскадов и «магии»
Persistence context как явление отсутствует — но нужна дисциплина транзакции и явных вызовов

Эта таблица важна не тем, что её надо выучить. Она важна тем, что вы начинаете задавать правильный первый вопрос: “мне нужна managed-модель или мне нужен эффект в БД?”. Как только вы это спрашиваете, половина ошибок исчезает сама — как только вы перестаёте считать Hibernate «магией», которая обязана всё угадывать.

3. Product: массовый статус и цена

Product в нашем проекте — звезда большинства лабораторий. На нём удобно показывать fetching, soft delete, Envers и многое другое. Но сегодня Product выступает в роли «массовой правки каталога», которая в реальной жизни случается постоянно: распродажа, закрытие линейки, скрытие legacy-ассортимента, заморозка товаров по регуляторным причинам. И каждый раз вопрос один: нам нужна логика “на каждый товар отдельно” или “один запрос и готово”?

С точки зрения разработчика это очень соблазнительное место, чтобы написать красивый Java-код: загрузить список, пробежаться, применить правила, вызвать save (чтобы точно, “на всякий случай”), и уйти с чувством выполненного долга. Но именно тут Hibernate заставляет вас взрослеть: цена этого “на всякий случай” может быть большой, особенно если правило по сути одинаковое для всех строк.

Bulk update для «одного правила на всех»

Если бизнес-идея звучит как “всем ACTIVE поставить HIDDEN” или “всем ACTIVE применить один коэффициент цены”, Product почти всегда становится bulk-кандидатом. Здесь операция описывает множество строк, а не поведение отдельного объекта, поэтому грузить каждый Product в память чаще всего просто дорого и лишне.

Если такой bulk завернули в Spring Data repository, физика не меняется: это всё равно mutation query, а после неё всё ещё нужно помнить про flush → bulk → clear/refresh → reread.

entity-loop для логики на каждом объекте

Когда решение зависит от SKU, доменных проверок, helper-методов, состояния связей или разных правил для разных товаров, bulk перестаёт быть естественным. Тогда managed entity-loop честнее: вы действительно работаете с конкретными объектами и их поведением, а не с множеством строк по одному шаблону.

Цена этого выбора никуда не исчезает — память, snapshots, dirty checking, дисциплина flush/clear на больших объёмах. Но если бизнес-правило живёт на уровне конкретной сущности, это нормальная цена, а не “провал оптимизации”.

StatelessSession для построчной обработки без раздувания PC

Иногда логика всё ещё построчная, но полноценный managed-контекст для неё не нужен. Тогда StatelessSession даёт промежуточный вариант: вы проходите по товарам по одному, но без first-level cache и automatic dirty checking.

Платите вы уже не памятью persistence context, а явностью: транзакция открывается явно, update()/delete() вызываются явно, каскадной “магии” нет. Это хороший режим для технических массовых job-ов, а не для обычного сервисного кода каталога.

4. InventorySnapshot: очистка истории

InventorySnapshot — это сущность, которая по своей природе стремится стать большой. Исторические данные любят копиться: каждый день новые строки, потом ещё и ещё, и в какой-то момент таблица начинает жить отдельной жизнью. И вот тут появляется типичный maintenance-вопрос: “удалить всё старше N дней”. Это почти учебник по bulk delete: одна граница по дате, одно действие, никакой доменной магии не нужно.

Но, как и с товарами, важно понимать: иногда “удалить старое” — не просто порог по дате, а серия условий, исключений и “оставить по одному последнему на товар”. И тогда bulk delete может перестать быть достаточным. Мы рассмотрим оба взгляда, чтобы вы научились задавать правильный вопрос, а не угадывать инструмент по настроению.

Порог по дате — почти всегда bulk delete

Если правило звучит как “удалить все строки, где snapshotDate < border”, cleanup почти всегда относится к bulk delete. Смысл операции здесь совпадает с delete ... where ...: не нужен ни один InventorySnapshot как объект, не нужен remove() в цикле, не нужно раздувать persistence context тысячами строк, которые вы тут же выбросите.

Если такой delete оформлен методом репозитория, это всё равно тот же bulk с теми же правилами для stale-состояния.

Когда cleanup зависит от каждой строки

Если cleanup зависит от нескольких полей, исключений или расчётов по каждой записи, выбор снова раскалывается на два варианта. Нужны lifecycle агрегата, связи и обычная managed-модель — остаёмся в entity-loop. Нужен просто построчный технический проход по большим объёмам без раздувания контекста — честнее взять StatelessSession.

Для InventorySnapshot второй вариант встречается особенно часто: здесь обычно важнее быстро и предсказуемо обработать много строк, чем жить в полном ORM-lifecycle каждой записи.

5. Мини-алгоритм выбора

Даже когда вы всё поняли теоретически, в реальной работе мозг иногда делает так: “тут 1000 строк… значит bulk… хотя… а вдруг…”. Чтобы этого избежать, полезно иметь простой алгоритм выбора. Не идеальный, не математический, но достаточно честный, чтобы не ошибаться на каждом втором pull request.

Ниже — один из вариантов в виде схемы. Это не “единственно верный путь”, а способ быстро прийти к первому осмысленному решению и уже потом уточнить детали по SQL-логу.

flowchart TD
    A[Нужно массово изменить/удалить данные] --> B{Логика зависит
от каждой строки?} B -->|Нет, одно правило| C[Bulk update/delete] C --> C1[Выполни bulk] C1 --> C2[После bulk: flush/clear или refresh + перечитать] B -->|Да, нужна проверка/вычисление| D{Нужен managed-граф
ORM-магия: каскады,
helper-методы?} D -->|Да| E[Entity-loop в обычной Session/EntityManager] E --> E1[Следи за размером context; при больших объёмах думай про flush/clear] D -->|Нет, построчно и явно| F{Объём большой и
PC раздувать нельзя?} F -->|Да| G[StatelessSession] F -->|Нет| E

Важный нюанс: этот алгоритм начинается не с “сколько строк”, а с “какая семантика”. Размер данных — это второй вопрос. Если вы начнёте с размера, вы будете выбирать инструмент «по страху» (или “по моде”), а не по смыслу операции. Hibernate такое любит наказывать: иногда не сразу, а через пару месяцев, когда данные станут больше.

6. Типичные ошибки при выборе подхода

Ошибки этого дня редко выглядят как “компилятор ругается”. Обычно всё компилируется и даже проходит happy-path тесты. А потом через неделю кто-то говорит: “у нас странно: после массовой операции UI показывает старые значения” или “почему сервис вдруг стал есть 2 гига памяти”. И вы начинаете узнавать в этом себя.

Ошибка №1: выбирать инструмент только по скорости и забывать про смысл операции.
Новичок часто видит bulk и думает: “О, быстрее — значит всегда лучше”. Но bulk — это про одинаковую массовую правку, а не про доменную логику. Если вам важно соблюсти правила на уровне каждой сущности, bulk превращает бизнес-логику в SQL-строку и делает сопровождение больнее. Правильное мышление начинается с вопроса: “мне нужен managed объект или эффект в БД?”.

Ошибка №2: сделать bulk и продолжить работать со старыми объектами как ни в чём не бывало.
Это классика stale persistence context: вы обновили статус товаров bulk-запросом, а потом читаете product.getStatus() у сущности, которую загрузили до bulk. И получаете старое значение. Не потому что Hibernate “сломался”, а потому что он честно не обязан обновлять ваши объекты, когда вы изменили БД в обход managed-модели. Лекарство простое: либо clear() и перечитать, либо точечный refresh() (если реально нужен один объект).

Ошибка №3: использовать StatelessSession, но ожидать от него automatic dirty checking.
Это особый вид боли: код выглядит “как обычно”, вы меняете поле, выходите из блока try, а в базе — тишина. Потому что в stateless-модели изменение поля — это просто изменение Java-объекта. Hibernate за ним не следит. Если выбрали StatelessSession, то “явный update()” — не опция, а обязательная часть договора.

Ошибка №4: смешивать в одном длинном методе чтение, bulk и дальнейшую бизнес-логику без явных фаз.
Bulk операции требуют дисциплины: “до bulk” и “после bulk” — это две разные реальности для persistence context. Когда вы впихиваете всё в один метод без разделения, вы сами создаёте себе ловушку: сложно понять, какие данные ещё managed, какие уже stale, а какие вообще должны быть перечитаны. Хороший код после bulk выглядит как короткая последовательность шагов: flush → bulk → clear → reread.

Ошибка №5: пытаться решить всё StatelessSession, потому что «там меньше магии».
Иногда после знакомства со stateless подходом возникает желание “выкинуть ORM из ORM” и писать всё явно: get/update/delete везде. Это обычно приводит к тому, что вы добровольно теряете преимущества Hibernate в обычной бизнес-логике: каскады, managed-граф, удобные транзакционные сценарии. StatelessSession — как огнетушитель: он должен быть рядом и иногда спасает, но жить в обнимку с ним — сомнительная идея.

1
Задача
Hibernate deep-dive, 24 уровень, 4 лекция
Недоступна
Два сценария для Product: entity-loop и bulk query
Два сценария для Product: entity-loop и bulk query
1
Задача
Hibernate deep-dive, 24 уровень, 4 лекция
Недоступна
Скрытие выбранных товаров по списку id через StatelessSession
Скрытие выбранных товаров по списку id через StatelessSession
1
Опрос
Массовые операции, 24 уровень, 4 лекция
Недоступен
Массовые операции
Bulk-операции и кэш
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ