hibernate/hibernate-orm · error · StaleObjectStateException
%s for entity %s#%s
Error message
%s for entity %s#%s
What it means
Optimistic-locking failure during mutation of an identified table: Expectation.verifyOutcome raised StaleStateException, and because the mutating table is not optional a 0-row UPDATE/DELETE is fatal, so Checkers rethrows StaleObjectStateException (message '<reason> for entity <entityName>#<id>') carrying the navigable path and id. It means the row was updated or deleted by another transaction, or the id/unsaved-value mapping made Hibernate update a row that never existed.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/action/queue/spi/bind/Checkers.java:48
TableDescriptor mutatingTable,
Object id,
String sqlString,
SessionFactoryImplementor sessionFactory) {
try {
expectation.verifyOutcome(
affectedRowCount,
null,
batchPosition,
sqlString
);
}
catch (StaleStateException e) {
if ( !mutatingTable.isOptional() && affectedRowCount == 0 ) {
final StatisticsImplementor statistics = sessionFactory.getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( mutationTarget.getNavigableRole().getFullPath() );
}
throw new StaleObjectStateException( mutationTarget.getNavigableRole().getFullPath(), id, e );
}
return false;
}
catch (TooManyRowsAffectedException e) {
throw new HibernateException(
String.format(
Locale.ROOT,
"Duplicate identifier in table (%s) - %s#%s",
mutatingTable.name(),
mutationTarget.getNavigableRole().getFullPath(),
id
)
);
}
catch (Throwable t) {
return false;
}
View on GitHub (pinned to fad1729dce)
Solutions
- Treat it as a business conflict: catch StaleObjectStateException, roll back, reload the latest state and reapply the user's changes (or ask the user to merge).
- If the row was legitimately deleted elsewhere, handle that event instead of retrying.
- If Hibernate should never have updated, audit the id strategy: is the entity new but carrying an id (wrong unsaved-value / persist vs merge misuse)?
- If rows may legitimately be absent from this table, map it optional (e.g. @Table(optional = true) semantics) so a 0-row result returns false instead of throwing.
Example fix
// before
session.merge(order); // throws StaleObjectStateException on conflict
// after - detect, roll back, reapply on fresh state
try {
session.merge(order);
tx.commit();
}
catch (StaleObjectStateException e) {
tx.rollback();
Order fresh = newSession.get(Order.class, order.getId());
fresh.setQty(order.getQty()); // reapply and commit
} Defensive patterns
Strategy: retry
Validate before calling
// optional pre-check: confirm the row still exists before editing in a long conversation boolean exists = session.find(Order.class, orderId) != null;
Try / catch
for (int attempt = 0; attempt < 3; attempt++) {
try {
tx = em.getTransaction(); tx.begin();
Order o = em.find(Order.class, orderId);
o.setQty(newQty);
tx.commit();
break;
}
catch (StaleObjectStateException e) {
if (tx.isActive()) tx.rollback();
// reload latest state and reapply, or give up and report the conflict
}
} Prevention
- Add @Version to shared entities so conflicts surface as this exception instead of silent lost updates
- Keep transactions short; do not hold entities across user think-time
- Use merge with fresh state after rollback instead of reusing the stale instance
When it happens
Trigger: Concurrent transactions modifying the same @Version row; the row deleted outside the session (another job, manual SQL) before flush; an entity saved with an assigned id but never inserted, so the UPDATE matches zero rows.
Common situations: Two users editing the same record (lost-update detection working as designed); batch jobs deleting rows out from under an open session; assigned-id generators plus wrong unsaved-value configuration; secondary table rows absent while mapped as non-optional.
Related errors
- <causeMessage> for entity [<entityName> with id '<id>']
- 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/efdcf444d094d145.
Report an issue: GitHub.