{"record":{"id":"bff6fab3ee648221","repo":"hibernate/hibernate-orm","slug":"batch-update-returned-unexpected-row-count-from-up-bff6fa","errorCode":null,"errorMessage":"Batch update returned unexpected row count from update {} (expected row count {} but was {})","messagePattern":"Batch update returned unexpected row count from update (.+?) \\(expected row count (.+?) but was (.+?)\\)","errorType":"exception","errorClass":"BatchedTooManyRowsAffectedException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java","lineNumber":81,"sourceCode":"\t}\n\n\tstatic void checkBatched(int expectedRowCount, int rowCount, int batchPosition, String sql) {\n\t\tswitch (rowCount) {\n\t\t\tcase EXECUTE_FAILED:\n\t\t\t\tthrow new BatchFailedException( \"Batch update failed: \" + batchPosition );\n\t\t\tcase SUCCESS_NO_INFO:\n\t\t\t\tBATCH_MESSAGE_LOGGER.batchSuccessUnknown( batchPosition );\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif ( expectedRowCount > rowCount ) {\n\t\t\t\t\tthrow new StaleStateException(\n\t\t\t\t\t\t\t\"Batch update returned unexpected row count from update \" + batchPosition\n\t\t\t\t\t\t\t\t\t+ actualVsExpected( expectedRowCount, rowCount )\n\t\t\t\t\t\t\t\t\t+ \" [\" + sql + \"]\"\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\telse if ( expectedRowCount < rowCount ) {\n\t\t\t\t\tthrow new BatchedTooManyRowsAffectedException(\n\t\t\t\t\t\t\t\"Batch update returned unexpected row count from update \" + batchPosition\n\t\t\t\t\t\t\t\t\t+ actualVsExpected( expectedRowCount, rowCount ),\n\t\t\t\t\t\t\texpectedRowCount, rowCount, batchPosition );\n\t\t\t\t}\n\t\t}\n\t}\n\n\tstatic void checkNonBatched(int expectedRowCount, int rowCount, String sql) {\n\t\tif ( expectedRowCount > rowCount ) {\n\t\t\tthrow new StaleStateException(\n\t\t\t\t\t\"Unexpected row count\"\n\t\t\t\t\t\t\t+ actualVsExpected( expectedRowCount, rowCount )\n\t\t\t\t\t\t\t+ \" [\" + sql + \"]\"\n\t\t\t);\n\t\t}\n\t\tif ( expectedRowCount < rowCount ) {\n\t\t\tthrow new TooManyRowsAffectedException(\n\t\t\t\t\t\"Unexpected row count\"","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before\n@SQLDelete(sql = \"delete from Person where tenantId = ?\", callable = false) // matches many rows\n\n// after\n@SQLDelete(sql = \"delete from Person where id = ?\") // exactly one row per statement","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    em.flush();\n} catch (org.hibernate.jdbc.BatchedTooManyRowsAffectedException e) {\n    // e.getExpectedRowCount() vs e.getActualRowCount() at e.getBatchPosition()\n    // inspect triggers/cascades on the affected table before retrying\n    throw e;\n}","preventionTips":["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"],"tags":["batching","custom-sql","row-count","triggers"],"backgroundTag":"unexpected-row-count","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}