1. Mass operations: one size does not fit all
When you first run into the task of “updating 50 000 rows”, your brain starts looking for one universal recipe. You want to believe there is some magic button called “Make it fast”, and that it is named either “batching”, “bulk”, or “some clever Hibernate mode”. Reality is less glamorous: different scenarios require different semantics for changing data, and only after that do you choose the execution technique.
At this point we already have three different modes: managed entity-loop, bulk mutation query, and StatelessSession. So from here on, it is more important not to re-explain the mechanics of each one, but to learn how to quickly choose the right approach based on the meaning of the operation, the data volume, and the cost of the persistence context.
Imagine you need to change the notices in the entrances of a large neighborhood. Sometimes you really need to go into every entrance, look at what is posted there, and carefully replace only what matches the condition — that is entity-loop. Sometimes you just need to change the template on the server, and all the notices on the electronic boards are updated at once — that is a bulk update. And sometimes you want to walk through the entrances, but without a “visit log” and without keeping in your head the history of all the entrances you have already visited — that is exactly StatelessSession.
In our Commerce Persistence Lab project, these situations look very real: products (Product) sometimes need to be hidden or “repriced” in bulk, and historical inventory snapshots (InventorySnapshot) periodically need to be cleaned up so the table does not turn into a museum of endless history. And this is where it is important not to start treating every problem with one tool just because it sounds cooler.
2. Table: entity-loop, bulk, and StatelessSession
To keep your head from turning the choice into a philosophical debate, it helps to look at these tools once as three different contracts with Hibernate. In one contract, Hibernate takes on many responsibilities and charges you for them in resources. In another, you say “I’ll do it myself,” and Hibernate just executes SQL. In the third, you also say “I’ll do it myself,” but you still remain inside the Hibernate API, just without the persistence context.
Below is a “cheat sheet for the wall” (or for a second monitor, next to the SQL log, so you can feel like a grown-up developer even if the coffee has gone cold):
| Approach | What actually happens | When it is natural | Main cost/risk | What to do with the persistence context |
|---|---|---|---|---|
entity-loop (regular /) |
Load managed entities → change fields → dirty checking → |
Logic depends on each row, you need domain checks, helper methods, cascades, and a familiar lifecycle | Many objects in memory, snapshots, expensive dirty checking, risk of “forgetting to clear in a big loop” | Watch the size of the context; in large loops think about |
bulk / (mutation query) |
One query changes/deletes rows in the database without loading entities | “One rule for all” by condition, technical mass edits, cleanup | Stale persistence context: old values remain in memory; also bypasses the managed model | Usually you need an explicit protocol: (if there were changes) → bulk → or → reread |
| StatelessSession | No first-level cache, no automatic dirty checking; operations are called explicitly | You need row-by-row processing, but not a managed graph; you want to avoid bloating the persistence context | It is easy to “save nothing” if you forget /; no cascades and no “magic” |
The persistence context as a concept does not exist — but transaction discipline and explicit calls are required |
This table matters not because you need to memorize it. It matters because it gets you asking the right first question: “Do I need a managed model, or do I need an effect in the database?” Once you ask that, half the mistakes disappear on their own — as soon as you stop treating Hibernate like “magic” that is supposed to guess everything.
3. Product: bulk status and price
Product in our project is the star of most labs. It is a convenient example for fetching, soft delete, Envers, and much more. But today Product is playing the role of a “mass catalog edit”, which happens all the time in real life: sales, product-line shutdowns, hiding a legacy assortment, freezing products for regulatory reasons. And every time the question is the same: do we need “one-by-one logic” or “one query and done”?
From a developer’s point of view, this is a very tempting place to write beautiful Java code: load a list, loop through it, apply rules, call save (just to be safe, “in case it matters”), and walk away feeling accomplished. But this is exactly where Hibernate forces you to grow up: the cost of that “just in case” can be high, especially if the rule is essentially the same for all rows.
Bulk update for “one rule for all”
If the business idea sounds like “set HIDDEN for all ACTIVE” or “apply one price coefficient to all ACTIVE”, Product is almost always a bulk candidate. Here the operation describes many rows rather than the behavior of a single object, so loading each Product into memory is usually just expensive and unnecessary.
If such a bulk operation is wrapped in a Spring Data repository, the physics do not change: it is still a mutation query, and after it you still need to remember the protocol flush → bulk → clear/refresh → reread.
entity-loop for logic on each object
When the decision depends on the SKU, domain checks, helper methods, relationship state, or different rules for different products, bulk stops being natural. Then a managed entity-loop is more honest: you are truly working with concrete objects and their behavior, not with many rows under one template.
The cost of this choice does not disappear — memory, snapshots, dirty checking, and discipline around flush/clear on large volumes. But if the business rule lives at the level of a concrete entity, that is a normal cost, not a “performance failure”.
StatelessSession for row-by-row processing without bloating the PC
Sometimes the logic is still row-by-row, but it does not need a full managed context. Then StatelessSession gives you an intermediate option: you process products one at a time, but without first-level cache and automatic dirty checking.
What you pay now is not persistence-context memory, but explicitness: the transaction is opened explicitly, update()/delete() are called explicitly, and there is no cascade “magic”. This is a good mode for technical mass jobs, not for regular catalog service code.
4. InventorySnapshot: cleaning up history
InventorySnapshot is an entity that naturally wants to grow large. Historical data loves to accumulate: new rows every day, then more and more, and at some point the table starts living a life of its own. And that is when the typical maintenance question appears: “delete everything older than N days”. This is almost a textbook bulk delete: one date boundary, one action, no domain magic needed.
But, as with products, it is important to understand: sometimes “delete old data” is not just a date threshold, but a series of conditions, exceptions, and “keep the latest one per product”. And in that case bulk delete may stop being enough. We will look at both perspectives so you learn to ask the right question instead of guessing the tool by mood.
A date threshold — almost always bulk delete
If the rule sounds like “delete all rows where snapshotDate < border”, cleanup almost always belongs to bulk delete. The meaning of the operation here matches delete ... where ...: you do not need a single InventorySnapshot as an object, you do not need remove() in a loop, and you do not need to bloat the persistence context with thousands of rows that you are about to throw away.
If such a delete is implemented as a repository method, it is still the same bulk operation with the same stale-state rules.
When cleanup depends on each row
If cleanup depends on multiple fields, exceptions, or calculations per record, the choice splits again into two options. If you need the aggregate lifecycle, relationships, and the regular managed model, stay with entity-loop. If you just need a row-by-row technical pass over a large volume without bloating the context, it is more honest to take StatelessSession.
For InventorySnapshot, the second option is especially common: here it is usually more important to process many rows quickly and predictably than to live in the full ORM lifecycle of each record.
5. Mini selection algorithm
Even when you understand everything in theory, in real work the brain sometimes does this: “there are 1000 rows here... so bulk... although... what if...”. To avoid that, it helps to have a simple selection algorithm. Not perfect, not mathematical, but honest enough not to get things wrong every second pull request.
Below is one option in diagram form. It is not “the only right path”, but a way to reach an initial meaningful decision quickly and then refine the details from the SQL log.
flowchart TD
A[Need to mass change/delete data] --> B{Does the logic depend
on each row?}
B -->|No, one rule| C[Bulk update/delete]
C --> C1[Perform bulk]
C1 --> C2[After bulk: flush/clear or refresh + reread]
B -->|Yes, checks/calculations needed| D{Need a managed graph?
ORM magic: cascades,
helper methods?}
D -->|Yes| E[Entity-loop in a regular Session/EntityManager]
E --> E1[Watch context size; think about flush/clear for large volumes]
D -->|No, row-by-row and explicit| F{Volume is large and
PC must not bloat?}
F -->|Yes| G[StatelessSession]
F -->|No| E
An important nuance: this algorithm starts not with “how many rows”, but with “what semantics”. Data size is the second question. If you start with size, you will choose the tool “out of fear” (or “because it is trendy”), not by the meaning of the operation. Hibernate likes to punish that: sometimes not immediately, but a couple of months later, when the data gets bigger.
6. Common mistakes when choosing an approach
The mistakes of this day rarely look like “the compiler is complaining”. Usually everything compiles and even passes happy-path tests. And then a week later someone says: “it is weird, after a mass operation the UI shows old values” or “why did the service suddenly start using 2 GB of memory?” And you start recognizing yourself in that.
Mistake #1: choosing a tool only by speed and forgetting the meaning of the operation.
Beginners often see bulk and think: “Oh, faster means always better.” But bulk is about the same mass edit applied to many rows, not about domain logic. If you need to honor rules at the level of each entity, bulk turns business logic into a SQL string and makes maintenance harder. The right mindset starts with the question: “Do I need a managed object, or do I need an effect in the database?”.
Mistake #2: do a bulk operation and keep working with the old objects as if nothing happened.
This is the classic stale persistence context: you updated the product status with a bulk query, and then you read product.getStatus() on an entity that was loaded before the bulk operation. And you get the old value. Not because Hibernate “broke”, but because it is not obligated to update your objects when you change the database outside the managed model. The fix is simple: either call clear() and reread, or do a targeted refresh() (if you really need only one object).
Mistake #3: use StatelessSession but expect automatic dirty checking from it.
This is a special kind of pain: the code looks “normal”, you change a field, leave the try block, and the database stays silent. Because in the stateless model, changing a field is just changing a Java object. Hibernate does not track it. If you chose StatelessSession, then an explicit update() is not an option, but a mandatory part of the contract.
Mistake #4: mix reading, bulk, and further business logic in one long method without explicit phases.
Bulk operations require discipline: “before bulk” and “after bulk” are two different realities for the persistence context. When you cram everything into one method without separation, you create a trap for yourself: it becomes hard to tell which data is still managed, which is already stale, and which should be reread altogether. Good code after bulk looks like a short sequence of steps: flush → bulk → clear → reread.
Mistake #5: try to solve everything with StatelessSession because “there is less magic there”.
Sometimes after getting familiar with the stateless approach, there is a temptation to “throw ORM out of ORM” and write everything explicitly: get/update/delete everywhere. This usually leads to you voluntarily losing the benefits of Hibernate in ordinary business logic: cascades, a managed graph, convenient transactional scenarios. StatelessSession is like a fire extinguisher: it should be nearby and sometimes saves you, but living side by side with it is a questionable idea.
GO TO FULL VERSION