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

  1. Catch StaleObjectStateException, refresh the entity (session.refresh or re-load) and retry the business operation
  2. Shorten the window between loading and locking (lock early at the start of the transaction)
  3. Ensure the entity actually carries a version column so the predicate is meaningful, and never mix stale detached versions with fresh locks
  4. 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

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


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/1e939a10406342f2. Report an issue: GitHub.