The 30 second answer
Hibernate writes changes to the database lazily, at flush time, not when I call save. So a duplicate key often fails at commit, outside the code that caused it. Exceptions are an ID generated by IDENTITY, which forces an insert immediately, and queries that trigger an automatic flush. If I need the error at a known point, I call saveAndFlush or flush inside the transaction.
Why it happens. The persistence context is a transactional write-behind cache: persist or Spring Data save only schedules the insert. Hibernate's default AUTO flush mode flushes before commit, before a JPQL or HQL query whose tables overlap the pending changes, and before a native SQL query. So the INSERT and its constraint check often run at commit, when the method that called save has already returned, and the exception surfaces from the transaction interceptor, not from your try block.
Exceptions to the rule.
- IDENTITY ids. With
GenerationType.IDENTITY, Hibernate must execute theINSERTright afterpersistto learn the id, so the constraint fails immediately. The cost is that JDBC insert batching is disabled for that entity; sequences allow both batching and late inserts. - Overlapping queries. A later query on the same table triggers an auto flush, so the error appears there, which looks unrelated.
Making the failure point explicit.
@Transactional
public Customer register(String email) {
try {
return repository.saveAndFlush(new Customer(email)); // INSERT runs now
} catch (DataIntegrityViolationException e) {
throw new EmailAlreadyUsedException(email, e);
}
}saveAndFlush or entityManager.flush() sends the SQL now, but does not commit: the transaction can still roll back. Flush synchronises state; commit makes it durable and visible.
Related trap: bulk updates. A JPQL UPDATE or DELETE goes straight to the database and is not reflected in entities already loaded in the persistence context, so later reads in the same transaction see stale values. Clear the context after bulk operations (@Modifying(clearAutomatically = true) in Spring Data).
Sources (4)
- Hibernate 6.6 User Guide: Flushing (AUTO flush before commit, overlapping queries and native SQL)
- Hibernate 6.6 User Guide: Identity generators (insert on persist, no JDBC insert batching)
- Hibernate 6.6 User Guide: Bulk update and delete (persistence context not updated)
- Spring Data JPA Javadoc: JpaRepository.saveAndFlush