Transactions, Dirty Checking and Flush in JPA
Transactions, Dirty Checking and Flush in JPA
JPA does not send SQL to the database the moment you call a setter. It waits, tracks changes, and sends them at the right moment. This behavior is powered by two core concepts: dirty checking and the flush mechanism, both operating within the boundaries of a transaction.
How Transactions Work
A transaction groups multiple database operations into a single atomic unit. Either all changes commit successfully, or none of them do. In Spring, you annotate a method with @Transactional to wrap it in a transaction:
@Service
public class OrderService {
@Transactional
public void placeOrder(Order order, List<OrderItem> items) {
entityManager.persist(order);
for (OrderItem item : items) {
item.setOrder(order);
entityManager.persist(item);
}
// All inserts happen at commit time, not here
}
}
The @Transactional annotation tells Spring to begin a transaction when the method starts and commit when it returns successfully. If an exception is thrown, the transaction rolls back. The SQL statements are batched and sent to the database at commit time, not on every persist() call.
Dirty Checking
When an entity is managed by the persistence context, Hibernate tracks every field. If you change a field after loading the entity, Hibernate marks it as "dirty." At flush time, it generates an UPDATE statement for every dirty entity automatically - no explicit update() call needed:
@Transactional
public void updatePrice(Long bookId, double newPrice) {
Book book = entityManager.find(Book.class, bookId);
// No update call needed - just modify the field
book.setPrice(newPrice);
// Hibernate detects the change and issues UPDATE ... SET price = ? WHERE id = ?
}
This is one of the biggest conveniences of ORM. Hibernate compares the current field values against the snapshot it took when the entity was loaded. Any difference triggers an UPDATE. If nothing changed, no SQL is sent.
Flush Modes
By default, Hibernate flushes automatically before a query executes and at transaction commit. You can also flush explicitly with entityManager.flush(). The flush operation synchronizes the persistence context with the database by sending all pending INSERT, UPDATE, and DELETE statements:
@Transactional
public void saveAndQuery(Order order) {
entityManager.persist(order);
entityManager.flush(); // Force SQL to be sent now
// The SELECT below sees the newly inserted row
Long count = entityManager.createQuery(
"SELECT COUNT(o) FROM Order o", Long.class
).getSingleResult();
}
Without the explicit flush, Hibernate's auto-flush would detect that your query touches the Order table and flush before executing the SELECT. The explicit flush makes the intent clear and avoids surprises.
FlushType and Ordering
Hibernate flushes entities in a specific order: first deletes, then inserts, then updates. This ordering matters for foreign key constraints. When you flush, Hibernate also checks queries for auto-flush by comparing the query's affected tables against the dirty entities.
Real-World Scenario
In a banking application, a transfer operation debits one account and credits another. Both updates must succeed or fail together. The @Transactional annotation ensures atomicity. Dirty checking means you simply adjust the balance fields and Hibernate handles the SQL. If an exception occurs mid-transfer, the entire transaction rolls back, leaving no partial state in the database.
Key Points
@Transactionalwraps a method in a transaction - all changes commit atomically or roll back on failure.- Dirty checking automatically detects changed fields on managed entities and generates UPDATE statements.
- By default, Hibernate flushes before queries and at transaction commit.
entityManager.flush()forces pending SQL to be sent to the database immediately.- Flush ordering respects foreign key constraints: deletes, then inserts, then updates.