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
- Tighten the custom SQL predicate so a statement affects exactly the entity's row(s)
- 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)
- 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
- Write custom SQL predicates that touch exactly one row per entity (by id + version)
- Do not double-manage cascades in both ORM mappings and DB triggers/FKs
- Document any tables where triggers legitimately alter row counts and use @Expectation(none) there
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
- Unexpected row count (expected row count {} but was {})
- write expression must contain exactly one value placeholder
- Unable to locate parameter `%s.%s` for %s - %s : %s
- Could not locate binding [%s : %s]
- The database returned no natively generated values : {}
AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22).
Data as JSON: /api/errors/bff6fab3ee648221.
Report an issue: GitHub.