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
- Make the custom SQL predicate unique (delete/update by primary key and version only)
- Remove overlap between ORM cascades and DB-side triggers/cascades so one operation changes exactly one row
- 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
- Make custom update/delete SQL unique per entity (primary key + version)
- Model inheritance deletes so each statement affects its own table row
- Smoke-test custom SQL row counts in CI against production-shaped data
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
- Batch update returned unexpected row count from update {} (e
- Unable to locate parameter `%s.%s` for %s - %s : %s
- cannot recreate collection while filter is enabled: " + coll
- Instance of '" + entityName + "' references an unsaved trans
- Action was vetoed: " + entityAction
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/655add51fe0bbac0.
Report an issue: GitHub.