Look at this Spring service. Eight lines, nothing exotic, no annotations missing as far as a junior reviewer can tell. Before reading on, predict what happens when something calls markActive(42) against a real database.
@Service
public class UserService {
private final UserRepository userRepo;
public UserService(UserRepository r) { this.userRepo = r; }
public User markActive(Long id) {
User u = userRepo.findById(id).orElseThrow();
u.setStatus("ACTIVE");
return u;
}
}
If you said “an UPDATE statement fires”, you would be wrong. There is no @Transactional on the method, so Spring Data opens a session for the duration of findById, returns the entity, and closes the session. By the time u.setStatus("ACTIVE") runs, u is no longer managed. The mutation exists only in JVM memory. There is no flush, no dirty check, no UPDATE.
This is the kind of bug you can stare at for an hour, because the code is almost right. It would be right if Hibernate worked the way most people imagine it does. The actual mechanics are stranger and more interesting — and once you see them, a long list of mysterious behaviours suddenly make sense: phantom UPDATEs, missing UPDATEs, NonUniqueObjectException, LazyInitializationException, the JPQL query that fires SQL you didn’t expect.
This post is a deep-dive on the persistence context: the in-memory state machine that decides what gets written, when, and why.
Continue reading The Hibernate 7 Persistence Context: How Hibernate Tracks Your Entities (and Where It Gets Surprising)
