The full picture
@Transactional
public void renameCustomer(long id, String name) {
Customer c = repo.findById(id).orElseThrow(); // managed
c.setName(name); // no save() call…
} // …flush at commit → UPDATE ✅
@Transactional
public void identity(long id) {
var a = repo.findById(id).get();
var b = repo.findById(id).get();
assert a == b; // same INSTANCE (1st-level cache)
}
public void detached(long id) { // NO transaction
Customer c = repo.findById(id).get(); // context closed after call
c.setName("x"); // detached — silently NOT saved
}- Flush ordering: JPA flushes pending changes before JPQL queries whose results could be affected (FlushModeType.AUTO) — why a 'read' can suddenly emit UPDATEs in your SQL log.
- save() semantics (Spring Data):
persistfor new,mergefor detached — merge copies state onto a managed instance and returns it; keeping the old reference is the classic detached-entity bug. - Long transactions + big contexts: every managed entity is snapshot-held; loading 100k rows in one TX means dirty-check overhead + memory. Batch jobs need
flush()/clear()windows or stateless sessions. - Second-level cache is a different animal (shared, cross-transaction, invalidation complexity) — opt-in per entity, justify with read-mostly data.