Dirty Checking
IntermediateHibernate tracks managed entity snapshots; on flush it compares current state with the snapshot and issues UPDATE for changed fields — understand this to avoid unexpected SQL statements.
Overview
Dirty checking is Hibernate's automatic change detection mechanism. When you load an entity into a managed state (via `find()`, JPQL, or Spring Data repository), Hibernate stores a **snapshot** of its field values at that moment. When the persistence context is **flushed** (before a query that reads the same table, on `@Transactional` method return, or explicitly via `em.flush()`), Hibernate compares every managed entity's current state against its snapshot. Any field that differs is considered "dirty" and Hibernate generates an `UPDATE` statement for it. This is entirely automatic — you never need to call `save()` on a managed entity. The downside: for large entity graphs, comparing thousands of fields on every flush is expensive. Optimise with `@DynamicUpdate`, `@Immutable`, or read-only sessions.
Automatic Dirty Tracking in Action
Simply mutate a managed entity's fields within a `@Transactional` method — Hibernate automatically detects the change and issues an UPDATE on flush. No explicit `save()` call is needed for managed entities. Calling `save()` on an already-managed entity is a no-op (it just returns the same instance). Only detached entities (outside a transaction) require `merge()` or `save()`.
@Transactional
public void updatePrice(Long productId, BigDecimal newPrice) {
// Loaded entity is NOW MANAGED — Hibernate holds a snapshot
Product product = productRepo.findById(productId).orElseThrow();
// Mutate the field — Hibernate detects this as "dirty"
product.setPrice(newPrice);
product.setUpdatedAt(Instant.now());
// NO explicit save() needed!
// On @Transactional method exit:
// Hibernate flushes → compares current state to snapshot
// → snapshot.price != current.price → generates:
// UPDATE products SET price=?, updated_at=? WHERE id=?
}
// Pitfall: mutating outside a transaction
Product p = productRepo.findById(1L).orElseThrow(); // detached after method returns
p.setPrice(BigDecimal.TEN); // change is LOST — no persistence context
// Must call productRepo.save(p) to re-attach via merge@DynamicUpdate and Selective Column Updates
By default, Hibernate's UPDATE statement includes ALL columns — even unchanged ones. This wastes DB resources and can cause issues with optimistic locking or generated columns. `@DynamicUpdate` tells Hibernate to include only dirty (changed) columns in the UPDATE statement. This is especially useful for wide entities (many columns) where only a few change per request.
@Entity
@DynamicUpdate // UPDATE includes only changed columns
@Table(name = "products")
public class Product {
@Id Long id;
String name;
BigDecimal price;
String description; // 5000-char text — don't include unless changed
String category;
boolean active;
Instant updatedAt;
}
// Without @DynamicUpdate:
// UPDATE products SET name=?, price=?, description=?, category=?,
// active=?, updated_at=? WHERE id=?
// (all 6 columns every time)
// With @DynamicUpdate:
// UPDATE products SET price=?, updated_at=? WHERE id=?
// (only the 2 changed columns)
// Also useful: @DynamicInsert — only includes non-null columns in INSERT
@Entity
@DynamicInsert
public class Order { ... }Disabling Dirty Checking: @Immutable and Read-Only Sessions
For read-only queries, dirty checking is pure overhead. Two options: `@Immutable` on the entity (Hibernate never generates UPDATE/DELETE for it — any mutation attempt is silently ignored). Or load entities in a **read-only session** — Hibernate skips snapshotting entirely, saving memory and CPU for large result sets.
// @Immutable — completely prevents dirty checking for the entity
@Entity
@Immutable // no UPDATE/DELETE will ever be generated
public class Country {
@Id Long id;
String code;
String name;
}
// Read-only via Spring Data @QueryHints
@QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true"))
List<Product> findAllByCategory(String category);
// Hibernate skips snapshot creation → less memory, faster flush
// Read-only via EntityManager directly
entityManager.setProperty("org.hibernate.readOnly", true);
List<Product> products = em.createQuery("SELECT p FROM Product p", Product.class)
.setHint("org.hibernate.readOnly", true)
.getResultList();
// Hibernate Statistics — verify dirty checking cost
Statistics stats = sessionFactory.getStatistics();
log.info("Dirty checked entities: {}", stats.getEntityUpdateCount());Key Points to Remember
- 1Hibernate stores a snapshot on entity load; on flush it compares and generates UPDATEs for dirty fields
- 2No explicit save() needed for managed entities — mutation within @Transactional is enough
- 3Flush triggers: before same-table query, on @Transactional commit, or explicit em.flush()
- 4@DynamicUpdate generates UPDATE with only changed columns — reduces DB overhead for wide entities
- 5@Immutable prevents any UPDATE/DELETE generation — ideal for reference/lookup tables
- 6Read-only query hints skip snapshot creation — saves memory and flush overhead for large reads
Interview Questions
Sign in to ask AriaWhat is dirty checking in Hibernate and when does it trigger?
If you update a managed entity but don't call save(), will the change persist?
What does @DynamicUpdate do and when would you use it?
How would you prevent dirty checking overhead for read-only queries?
What is the difference between a managed entity and a detached entity with respect to dirty checking?
Ask Aria about Dirty Checking
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.