hibernate/hibernate-orm · error · StaleObjectStateException
<causeMessage> for entity [<entityName> with id '<id>']
Error message
<causeMessage> for entity [<entityName> with id '<id>']
What it means
ModelMutationHelper executes insert/update/delete mutations and catches StaleStateException; when the mutating table is not optional and the JDBC driver reported 0 affected rows, it throws StaleObjectStateException(fullPath, id, cause). That exception's getMessage() composes '<causeMessage> for entity [EntityName with id '<id>']' — this is Hibernate's optimistic-locking conflict: the row was updated or deleted by another transaction since it was loaded.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/ModelMutationHelper.java:72
Object id,
SessionFactoryImplementor sessionFactory) {
try {
statementDetails.getExpectation().verifyOutcome(
affectedRowCount,
statementDetails.getStatement(),
batchPosition,
statementDetails.getSqlString()
);
return true;
}
catch (StaleStateException e) {
if ( !statementDetails.getMutatingTableDetails().isOptional() && affectedRowCount == 0 ) {
final String fullPath = mutationTarget.getNavigableRole().getFullPath();
final var statistics = sessionFactory.getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( fullPath );
}
throw new StaleObjectStateException( fullPath, id, e );
}
return false;
}
catch (TooManyRowsAffectedException e) {
throw new HibernateException(
String.format(
Locale.ROOT,
"Duplicate identifier in table (%s) - %s#%s",
statementDetails.getMutatingTableDetails().getTableName(),
mutationTarget.getNavigableRole().getFullPath(),
id
)
);
}
catch (Throwable t) {
return false;
}
}View on GitHub (pinned to fad1729dce)
Solutions
- Catch OptimisticLockException/StaleObjectStateException at the use-case boundary and handle the conflict: reload the entity and retry, or report 409/conflict to the user
- Verify @Version optimistic-locking is mapped to the actual DB column and that all writers update it
- With assigned identifiers, check unsaved-value configuration so existing rows are updated, not treated inconsistently
- Shorten conversations: load-modify-save within one transaction instead of holding entities across requests
Example fix
// before: unguarded merge
em.merge(doc);
// after: explicit conflict handling
try {
em.merge(doc);
em.flush();
}
catch (OptimisticLockException e) {
Doc fresh = em.find(Doc.class, doc.getId()); // reload and reapply or report conflict
throw new ConcurrentModificationException("doc " + doc.getId() + " changed", e);
} Defensive patterns
Strategy: retry
Validate before calling
// optimistic guard: check the version before flushing changes
Doc fresh = em.find(Doc.class, doc.getId());
if (fresh == null || fresh.getVersion() > doc.getVersion()) {
throw new ConcurrentModificationException("doc changed, reload before edit");
} Try / catch
catch (OptimisticLockException e) {
// reload latest state, reapply user changes, retry once with a backoff
} Prevention
- Map @Version columns and let all writers bump them
- Keep load-modify-save conversations short
- Handle OptimisticLockException explicitly at the service/UI boundary (retry or 409)
When it happens
Trigger: flush()/commit of an UPDATE or DELETE whose WHERE (id [+ version]) matched no row: a concurrent transaction committed changes first, the row was deleted, or the version/unsaved-value mapping is wrong so Hibernate compares stale criteria.
Common situations: Two users editing the same record; background jobs deleting entities out from under a loaded screen; long conversations with detached entities merged after the data changed; missing or misconfigured @Version columns.
Related errors
- %s for entity %s#%s
- Newer version [" + latestVersion + "] of entity [" + infoStr
- Row was already updated or deleted by another transaction
- Row was already updated or deleted by another transaction
- No rows were returned from JDBC query for versioned entity
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/46678fbf56d06015.
Report an issue: GitHub.