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=42

Dirty 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 save for 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).