hibernate/hibernate-orm · error · BatchedTooManyRowsAffectedException

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

The batched counterpart of the too-few-rows case: when a batched statement affects MORE rows than expected (expected 1, actual N), Hibernate throws BatchedTooManyRowsAffectedException carrying expectedRowCount, rowCount and batchPosition. Something besides the ORM's own predicate changed extra rows - usually database-side triggers, FK cascades, or an insufficiently restrictive custom SQL.

Source

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

	}

	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"
							+ actualVsExpected( expectedRowCount, rowCount )
							+ " [" + sql + "]"
			);
		}
		if ( expectedRowCount < rowCount ) {
			throw new TooManyRowsAffectedException(
					"Unexpected row count"

View on GitHub (pinned to fad1729dce)

Solutions

  1. Tighten the custom SQL predicate so a statement affects exactly the entity's row(s)
  2. Remove the duplication between DB-side triggers/cascades and ORM operations (or make the trigger's changes invisible to the count, e.g. INSTEAD OF triggers)
  3. If extra counts are expected by design, relax the check with @Expectation(none)

Example fix

// before
@SQLDelete(sql = "delete from Person where tenantId = ?", callable = false) // matches many rows

// after
@SQLDelete(sql = "delete from Person where id = ?") // exactly one row per statement
Defensive patterns

Strategy: try-catch

Try / catch

try {
    em.flush();
} catch (org.hibernate.jdbc.BatchedTooManyRowsAffectedException e) {
    // e.getExpectedRowCount() vs e.getActualRowCount() at e.getBatchPosition()
    // inspect triggers/cascades on the affected table before retrying
    throw e;
}

Prevention

When it happens

Trigger: Batched custom delete/update where an ON DELETE CASCADE or a database trigger performs additional changes on the same statement; update SQL missing a restrictive predicate so one statement matches several rows.

Common situations: Schema with FK ON DELETE CASCADE plus ORM-level cascade creating double effects; audit triggers rewriting rows; custom @SQLDelete with a predicate that is not unique per entity.

Related errors


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