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

Select-based pessimistic locking (AbstractSelectLockingStrategy) runs a 'select ... for update'-style statement and expects exactly one row back. When resultSet.next() returns false the row was not visible - deleted by a concurrent transaction, never committed, or the id/version simply does not exist - and Hibernate throws StaleObjectStateException, whose built-in message is 'Row was already updated or deleted by another transaction'. Statistics (optimisticFailure) are bumped when enabled.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractSelectLockingStrategy.java:108

			try {
				lockable.getIdentifierType().nullSafeSet( preparedStatement, id, 1, session );
				if ( lockable.isVersioned() ) {
					lockable.getVersionType().nullSafeSet(
							preparedStatement,
							version,
							lockable.getIdentifierType().getColumnSpan( factory.getRuntimeMetamodels() ) + 1,
							session
					);
				}

				final var resultSet = jdbcCoordinator.getResultSetReturn().extract( preparedStatement, sql );
				try {
					if ( !resultSet.next() ) {
						final var statistics = factory.getStatistics();
						if ( statistics.isStatisticsEnabled() ) {
							statistics.optimisticFailure( lockable.getEntityName() );
						}
						throw new StaleObjectStateException( lockable.getEntityName(), id );
					}
				}
				finally {
					jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( resultSet, preparedStatement );
				}
			}
			finally {
				jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( preparedStatement );
				jdbcCoordinator.afterStatementExecution();
			}
		}
		catch ( SQLException sqle ) {
			throw convertException( object, jdbcException( id, session, sqle, sql ) );
		}
	}

	private JDBCException jdbcException(Object id, SharedSessionContractImplementor session, SQLException sqle, String sql) {
		return session.getJdbcServices().getSqlExceptionHelper()

View on GitHub (pinned to fad1729dce)

Solutions

  1. Catch StaleObjectStateException and treat it as 'entity no longer exists' - reload and either skip, recreate, or surface a conflict to the user
  2. Verify existence right before locking when deletes are common (or re-check after the exception using session.find)
  3. Coordinate delete flows and lock flows so they cannot interleave on the same rows
  4. Log the entity name and identifier from the exception to identify which row vanished

Example fix

// before
session.lock(orderLine, LockMode.PESSIMISTIC_WRITE);

// after
try {
    session.lock(orderLine, LockMode.PESSIMISTIC_WRITE);
}
catch (StaleObjectStateException e) {
    OrderLine fresh = session.find(OrderLine.class, orderLine.getId());
    if (fresh == null) {
        // row deleted concurrently: skip or re-create
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Optional pre-check when deletes are expected
Object fresh = session.find(entityClass, id);
if (fresh == null) {
    // skip locking - row already gone
}

Try / catch

try {
    session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(entity);
}
catch (StaleObjectStateException e) {
    // row not visible: deleted or never committed - verify and handle
    if (session.find(entityClass, id) == null) {
        // treat as deleted: skip or compensate
    }
    else {
        // visible now (uncommitted earlier): retry the lock once
    }
}

Prevention

When it happens

Trigger: session.lock(entity, LockMode.PESSIMISTIC_WRITE) on an entity whose row another transaction has deleted or not yet committed; locking a detached entity whose id was removed; read-uncommitted/read-committed isolation hiding an uncommitted insert; wrong id passed after a merge. The select returns an empty result set and the exception carries entityName and id.

Common situations: Locking entities referenced by stale foreign keys (row already gone); concurrent delete flows racing lock flows; tests that lock rolled-back fixtures; isolation-level differences between environments making rows invisible.

Related errors


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