hibernate/hibernate-orm · error · StaleObjectStateException
Row was already updated or deleted by another transaction
Error message
Row was already updated or deleted by another transaction
What it means
Update-based pessimistic locking issues UPDATE ... set version=? where id=? and version=?; the constructor-supplied row must still exist with the expected version. When executeUpdate reports fewer than 0 affected rows, Hibernate throws StaleObjectStateException whose message is 'Row was already updated or deleted by another transaction' (built into StaleObjectStateException itself). So even though you asked for a pessimistic lock, the failure mode is a stale-state conflict detected by the version predicate.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java:89
versionType.nullSafeSet( preparedStatement, version, 1, session );
int offset = 2;
identifierType.nullSafeSet( preparedStatement, id, offset, session );
offset += identifierType.getColumnSpan( factory.getRuntimeMetamodels() );
if ( lockable.isVersioned() ) {
versionType.nullSafeSet( preparedStatement, version, offset, session );
}
final int affected = jdbcCoordinator.getResultSetReturn().executeUpdate( preparedStatement, sql );
// todo: should this instead check for exactly one row modified?
if ( affected < 0 ) {
final var statistics = factory.getStatistics();
final String entityName = lockable.getEntityName();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( entityName );
}
throw new StaleObjectStateException( entityName, id );
}
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( preparedStatement );
jdbcCoordinator.afterStatementExecution();
}
}
catch ( SQLException e ) {
throw session.getJdbcServices().getSqlExceptionHelper().convert(
e,
"could not lock: " + infoString( lockable, id, session.getFactory() ),
sql
);
}
}
protected String generateLockString() {View on GitHub (pinned to fad1729dce)
Solutions
- Catch StaleObjectStateException, refresh the entity (session.refresh or re-load) and retry the business operation
- Shorten the window between loading and locking (lock early at the start of the transaction)
- Ensure the entity actually carries a version column so the predicate is meaningful, and never mix stale detached versions with fresh locks
- For hot rows, consider a dedicated lock table or database advisory locks instead of row updates
Example fix
// before
session.lock(person, LockMode.PESSIMISTIC_WRITE);
process(person);
// after
try {
session.lock(person, LockMode.PESSIMISTIC_WRITE);
process(person);
}
catch (StaleObjectStateException e) {
session.refresh(person); // reload current state
// re-apply or report the conflict
} Defensive patterns
Strategy: try-catch
Try / catch
try {
session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(person);
}
catch (StaleObjectStateException e) { // org.hibernate
// row updated/deleted by a concurrent transaction: reload and re-decide
session.refresh(person);
// re-apply the business operation or report a conflict
} Prevention
- Lock as early as possible in the transaction to shrink the read-to-lock window
- Catch StaleObjectStateException wherever rows are contended and translate it into a user-facing conflict message
- Do not reuse detached entities' version values for later locks - always lock managed, freshly loaded instances
When it happens
Trigger: Two transactions lock the same unversioned-check row via update locking: the first commits or deletes, then the second's locking UPDATE matches 0 rows (driver returns a negative count for 'no match') and the exception is thrown with the entity name and id. Also possible when the row was hard-deleted between read and lock, or the version value passed is stale.
Common situations: Race conditions between a background job and user requests updating the same row; delete-then-lock sequences in the same transaction; retrying operations after a long think time while another request modified the entity.
Related errors
- Could not obtain pessimistic lock
- Row was already updated or deleted by another transaction
- %s for entity %s#%s
- Spanner does not support no wait.
- Spanner does not support skip locked.
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/1e939a10406342f2.
Report an issue: GitHub.