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

  1. 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).
  2. If the row was legitimately deleted elsewhere, handle that event instead of retrying.
  3. 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)?
  4. 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

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


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