Careeroza
Dirty Checking
How Hibernate Knows What Changed
When Hibernate loads an entity, it keeps a snapshot: a copy of every column value. At flush time it compares each managed entity with its snapshot. Any difference makes the entity dirty, and Hibernate generates an UPDATE for it. Unchanged entities produce no SQL at all.
find(Product, 42) -> SELECT -> entity {stock=120, price=1499.00}
snapshot [120, 1499.00]
product.setStock(119) -> entity {stock=119, price=1499.00}
commit -> flush -> compare entity vs snapshot -> stock differs -> UPDATE products ... where id=42Dirty Checking, Simulated
Example
Three products are loaded, two are changed (one is "changed" back to its original value). Only the really modified one gets an UPDATE:
import java.math.BigDecimal;
import java.util.*;
public class Main {
public static void main(String[] args) {
Session s = new Session();
Product boat = s.load(1L, "Boat Rockerz 450", "1499.00", 120);
Product noise = s.load(2L, "Noise ColorFit Pro", "2299.00", 40);
Product cable = s.load(3L, "USB-C Cable", "199.00", 500);
boat.stock = 119; // real change
noise.price = new BigDecimal("1999.00");
noise.price = new BigDecimal("2299.00"); // set back: not dirty
// cable untouched
System.out.println("-- commit --");
s.flush();
}
}
class Product {
final Long id; String name; BigDecimal price; Integer stock;
Product(Long id, String name, BigDecimal price, Integer stock) {
this.id = id; this.name = name; this.price = price; this.stock = stock;
}
Object[] state() { return new Object[] { name, price, stock }; }
}
class Session {
private static final String[] COLUMNS = { "name", "price", "stock" };
private final Map<Long, Product> entities = new LinkedHashMap<>();
private final Map<Long, Object[]> snapshots = new HashMap<>();
Product load(Long id, String name, String price, int stock) {
Product p = new Product(id, name, new BigDecimal(price), stock);
entities.put(id, p);
snapshots.put(id, p.state()); // copy of loaded values
return p;
}
void flush() {
for (Product p : entities.values()) {
Object[] now = p.state(), then = snapshots.get(p.id);
List<String> changed = new ArrayList<>();
for (int i = 0; i < now.length; i++)
if (!Objects.equals(now[i], then[i])) changed.add(COLUMNS[i] + "=" + now[i]);
if (changed.isEmpty()) System.out.println("Product#" + p.id + ": not dirty, no SQL");
else {
System.out.println("update products set " + String.join(", ", changed) + " where id=" + p.id);
snapshots.put(p.id, now);
}
}
}
}Output
-- commit -- update products set stock=119 where id=1 Product#2: not dirty, no SQL Product#3: not dirty, no SQL
Note: By default real Hibernate writes all columns in the UPDATE (the SQL can then be cached and reused). Add @DynamicUpdate on the entity if you want only the changed columns, as in this simulation.
Consequences
- You never call
savefor a managed entity inside a transaction — changing fields is enough. - Changing an entity "just for display" inside a transaction (
product.setName(name.toUpperCase())) will be saved. Map to a DTO instead. - Dirty checking compares every managed entity at flush; loading 10,000 entities into one transaction makes flushes slow. Use read-only transactions or
clear()for big jobs (advance level).