Dirty Checking Internals

Snapshots

When Hibernate loads an entity it also stores a snapshot: an array with the loaded value of every property. At flush it compares each managed entity with its snapshot, property by property. Any difference produces an UPDATE. You never call save() for a managed entity.

Example

A simulation of flush-time dirty checking:

import java.util.*;

public class Main {
    static final String[] PROPS = {"name", "price", "stock"};

    static class Product {
        String name; int price; int stock;
        Product(String n, int p, int s) { name = n; price = p; stock = s; }
        Object[] state() { return new Object[]{name, price, stock}; }
    }

    public static void main(String[] args) {
        Map<Long, Product> managed = new LinkedHashMap<>();
        Map<Long, Object[]> snapshots = new HashMap<>();

        long[] ids = {1, 2, 3};
        Product[] loaded = { new Product("Kurta", 799, 20), new Product("Saree", 2499, 5), new Product("Jeans", 1299, 9) };
        for (int i = 0; i < ids.length; i++) {
            managed.put(ids[i], loaded[i]);
            snapshots.put(ids[i], loaded[i].state());      // copy taken at load time
        }

        managed.get(1L).stock = 19;                        // plain setters, no save()
        managed.get(3L).price = 1199;
        managed.get(3L).name = "Jeans";                    // same value: not dirty

        System.out.println("flush:");
        managed.forEach((id, p) -> {
            Object[] now = p.state(), then = snapshots.get(id);
            List<String> dirty = new ArrayList<>();
            for (int i = 0; i < PROPS.length; i++) if (!Objects.equals(now[i], then[i])) dirty.add(PROPS[i]);
            if (dirty.isEmpty()) System.out.println("  Product#" + id + " clean, no SQL");
            else System.out.println("  Product#" + id + " dirty " + dirty + " -> update products set name=?,price=?,stock=? where id=?");
        });
    }
}

Output

flush:
  Product#1 dirty [stock] -> update products set name=?,price=?,stock=? where id=?
  Product#2 clean, no SQL
  Product#3 dirty [price] -> update products set name=?,price=?,stock=? where id=?

Costs and Options

  • Dirty checking cost grows with the number of managed entities: 10,000 loaded entities = 10,000 comparisons at every flush (and AUTO may flush many times). Keep sessions small, or use readOnly.
  • By default the UPDATE contains all columns, so one prepared statement per entity can be reused and batched. @DynamicUpdate on the entity sends only changed columns: useful for wide tables or to reduce lock conflicts on some databases.
  • Mutable types (Date, arrays, JSON objects) are compared by value through their Hibernate type. Changing an object inside a JSON field without the type knowing can go undetected.
  • With bytecode enhancement, entities track their own dirty fields, so Hibernate skips the comparison (next topics).