hibernate/hibernate-orm · error · TooManyRowsAffectedException

Unexpected row count (expected row count {} but was {})

Error message

Unexpected row count (expected row count {} but was {})

What it means

checkNonBatched throws TooManyRowsAffectedException when a non-batched statement affects more rows than the ORM expected (expected 1, actual N). One logical entity operation physically touched several rows - either the SQL predicate is not unique, or the database (trigger, FK cascade, inheritance tables) changed extra rows within the same statement.

Source

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

				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"
							+ actualVsExpected( expectedRowCount, rowCount ),
					1, rowCount
			);
		}
	}

	private static String actualVsExpected(int expectedRowCount, int rowCount) {
		return " (expected row count " + expectedRowCount + " but was " + rowCount + ")";
	}

	private Expectations() {
	}
}

View on GitHub (pinned to fad1729dce)

Solutions

  1. Make the custom SQL predicate unique (delete/update by primary key and version only)
  2. Remove overlap between ORM cascades and DB-side triggers/cascades so one operation changes exactly one row
  3. If multi-row effects are intended, disable the count check for that operation with @Expectation(none)

Example fix

// before
@SQLUpdate(sql = "update Account set balance = ? where ownerId = ?") // owner may hold several accounts

// after
@SQLUpdate(sql = "update Account set balance = ? where id = ?") // exactly one row
Defensive patterns

Strategy: try-catch

Try / catch

try {
    em.flush();
} catch (org.hibernate.TooManyRowsAffectedException e) {
    // e.getExpectedRowCount()=1, e.getActualRowCount()=N: the SQL predicate is not unique
    // or a trigger/cascade changed extra rows - fix mapping or custom SQL
    throw e;
}

Prevention

When it happens

Trigger: Custom @SQLUpdate/@SQLDelete whose predicate matches multiple rows; joined-table inheritance deletes hitting both parent and child tables in one statement; DB triggers or ON DELETE CASCADE duplicating the ORM's own work.

Common situations: Custom SQL copied from a bulk-maintenance script without a primary-key predicate; schemas where triggers maintain denormalized copies; migration from single-table to joined inheritance leaving old SQL in place.

Related errors


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