hibernate/hibernate-orm · error · StaleStateException

Batch update returned unexpected row count from update {} (e

Error message

Batch update returned unexpected row count from update {} (expected row count {} but was {}) [{}]

What it means

In batched mode, when a statement reports fewer affected rows than expected (expected > actual, typically 0 for a versioned update), Hibernate throws StaleStateException with the batch position, expected-vs-actual counts, and the SQL. The database simply did not change the rows the ORM believed it should - usually because the row was already modified or deleted by another transaction.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java:74

		if ( statement instanceof CallableStatement callableStatement ) {
			return callableStatement;
		}
		else {
			throw new HibernateException( "Expectation.OutParameter operates exclusively on CallableStatements: "
					+ statement.getClass() );
		}
	}

	static void checkBatched(int expectedRowCount, int rowCount, int batchPosition, String sql) {
		switch (rowCount) {
			case EXECUTE_FAILED:
				throw new BatchFailedException( "Batch update failed: " + batchPosition );
			case SUCCESS_NO_INFO:
				BATCH_MESSAGE_LOGGER.batchSuccessUnknown( batchPosition );
				break;
			default:
				if ( expectedRowCount > rowCount ) {
					throw new StaleStateException(
							"Batch update returned unexpected row count from update " + batchPosition
									+ actualVsExpected( expectedRowCount, rowCount )
									+ " [" + sql + "]"
					);
				}
				else if ( expectedRowCount < rowCount ) {
					throw new BatchedTooManyRowsAffectedException(
							"Batch update returned unexpected row count from update " + batchPosition
									+ actualVsExpected( expectedRowCount, rowCount ),
							expectedRowCount, rowCount, batchPosition );
				}
		}
	}

	static void checkNonBatched(int expectedRowCount, int rowCount, String sql) {
		if ( expectedRowCount > rowCount ) {
			throw new StaleStateException(
					"Unexpected row count"

View on GitHub (pinned to fad1729dce)

Solutions

  1. Confirm the row still exists and its version matches before flushing (or re-attach with a fresh load)
  2. Catch StaleStateException / OptimisticLockException, reload the entity, reapply changes, retry the unit of work
  3. If the update legitimately affects 0 rows, use @Expectation(none) or adjust the custom SQL counts
  4. Audit for out-of-band deletes (DBA scripts, cascade jobs) running against the same tables

Example fix

// before
session.merge(detachedPerson);
session.flush(); // StaleStateException: row version changed / row gone

// after
Person fresh = session.find(Person.class, detachedPerson.getId());
if (fresh != null) { fresh.applyChangesFrom(detachedPerson); session.flush(); }
else { throw new ConcurrentModificationException("person deleted concurrently"); }
Defensive patterns

Strategy: try-catch

Validate before calling

// optional pre-flight for delete-heavy flows
Object version = em.createQuery("select p.version from Person p where p.id = :id", Object.class)
                  .setParameter("id", id)
                  .getSingleResultOrNull();
if (version == null) throw new IllegalStateException("row missing before update/delete");

Try / catch

try {
    em.flush();
} catch (org.hibernate.StaleStateException e) {
    em.clear();
    Person fresh = em.find(Person.class, id);
    if (fresh == null) {
        // row deleted elsewhere - treat as gone, do not retry the same operation
    } else {
        // reload current state, reapply user changes, retry once
    }
}

Prevention

When it happens

Trigger: Versioned (optimistic-lock) batched updates where the version no longer matches; session.delete() of an entity whose row was concurrently removed; custom @SQLUpdate returning 0; batched delete hitting zero rows.

Common situations: Concurrent modifications from another session/service; manual deletes/updates via SQL behind the ORM's back; long-lived detached objects flushed after external changes; load balancers routing two users onto the same row.

Related errors


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