hibernate/hibernate-orm · error · StaleObjectStateException
No rows were returned from JDBC query for versioned entity
Error message
No rows were returned from JDBC query for versioned entity
What it means
While applying a pessimistic lock via its locking SELECT, SqlAstBasedLockingStrategy received zero rows (org.hibernate.sql.results.spi.NoRowException): the row identified by id no longer exists - typically deleted concurrently between load and lock. For entities with an optimistic lock style other than NONE (@Version), Hibernate translates this into StaleObjectStateException with this message so callers treat it as an optimistic-lock failure; for unversioned entities the raw NoRowException propagates instead.
Source
Thrown at hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/SqlAstBasedLockingStrategy.java:206
);
} );
}
}
catch (LockTimeoutException lockTimeout) {
throw new PessimisticEntityLockException(
object,
String.format( Locale.ROOT, "Lock timeout exceeded attempting to lock row(s) for %s", object ),
lockTimeout
);
}
catch (NoRowException noRow) {
if ( !entityToLock.optimisticLockStyle().isNone() ) {
final String entityName = entityToLock.getEntityName();
final var statistics = session.getFactory().getStatistics();
if ( statistics.isStatisticsEnabled() ) {
statistics.optimisticFailure( entityName );
}
throw new StaleObjectStateException( entityName, id,
"No rows were returned from JDBC query for versioned entity" );
}
else {
throw noRow;
}
}
}
private static void handleRestriction(
Object value,
SelectableMapping jdbcValueMapping,
QuerySpec rootQuerySpec,
LoaderSqlAstCreationState sqlAstCreationState,
TableGroup rootTableGroup,
JdbcParameterBindings jdbcParameterBindings) {
final var jdbcParameter = new SqlTypedMappingJdbcParameter( jdbcValueMapping );
rootQuerySpec.applyPredicate(
new ComparisonPredicate(View on GitHub (pinned to fad1729dce)
Solutions
- Catch StaleObjectStateException / OptimisticLockException and re-load the entity (em.refresh won't work - use find again) or treat it as not-found
- Re-fetch a fresh managed instance before locking instead of locking a long-held detached reference
- If deletion is legitimate in your domain, branch on EntityNotFoundException-style handling instead of retrying
- For soft deletes, verify row-level filters do not hide rows from the locking select
Example fix
// before
session.lock(order, LockMode.PESSIMISTIC_WRITE); // row may be gone -> StaleObjectStateException
// after
try {
session.lock(order, LockMode.PESSIMISTIC_WRITE);
} catch (StaleObjectStateException e) {
Order fresh = session.find(Order.class, order.getId());
if (fresh == null) {
// row deleted concurrently: treat as not-found
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// verify the row still exists before requesting the lock
Object id = session.getIdentifier(entity);
boolean exists = session.createQuery("select 1 from Order o where o.id = :id", Integer.class)
.setParameter("id", id).getResultList().isEmpty() == false; Try / catch
try {
session.lock(order, LockMode.PESSIMISTIC_WRITE);
} catch (StaleObjectStateException e) {
// row vanished between load and lock: reload or treat as not-found
Order fresh = session.find(Order.class, order.getId());
if (fresh == null) { /* deleted concurrently */ }
} Prevention
- Lock freshly loaded entities in the same transaction rather than long-held references
- Handle deletes explicitly: coordinate delete and lock flows to avoid races
- Treat StaleObjectStateException from locks like OptimisticLockException in retry/refresh logic
- In work-queue patterns, tolerate items claimed-then-deleted by other workers
When it happens
Trigger: session.lock(entity, LockMode.PESSIMISTIC_WRITE) or em.find(id, PESSIMISTIC_WRITE) where another transaction deleted (hard delete, or @SQLDelete) the row after it was loaded; locking a detached/stale reference whose row is gone; follow-on locking racing a concurrent remove().
Common situations: Two users editing the same record with one deleting it; cleanup jobs purging rows that in-flight requests still reference; queue/claim patterns where another worker deleted the item; soft-delete filters hiding rows that the locking select expects.
Related errors
- Row was already updated or deleted by another transaction
- Newer version [" + latestVersion + "] of entity [" + infoStr
- %s for entity %s#%s
- Lock timeout exceeded attempting to lock row(s) for %s
- <causeMessage> for entity [<entityName> with id '<id>']
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/52cbd46a3293434b.
Report an issue: GitHub.