{"record":{"id":"b4d3485cd6e3d2f9","repo":"hibernate/hibernate-orm","slug":"unexpected-row-count-expected-row-count-but-wa","errorCode":null,"errorMessage":"Unexpected row count (expected row count {} but was {}) [{}]","messagePattern":"Unexpected row count \\(expected row count (.+?) but was (.+?)\\) \\[(.+?)\\]","errorType":"exception","errorClass":"StaleStateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java","lineNumber":91,"sourceCode":"\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\"\n\t\t\t\t\t\t\t+ actualVsExpected( expectedRowCount, rowCount ),\n\t\t\t\t\t1, rowCount\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate static String actualVsExpected(int expectedRowCount, int rowCount) {\n\t\treturn \" (expected row count \" + expectedRowCount + \" but was \" + rowCount + \")\";\n\t}\n","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/jdbc/Expectations.java#L73-L109","documentation":"checkNonBatched throws StaleStateException with expected-vs-actual counts and the SQL when a non-batched update/delete affects fewer rows than expected - classically zero. The ORM expected to change a row that is no longer there (or whose version no longer matches), which almost always means concurrent modification or deletion outside the current session.","triggerScenarios":"session.delete()/remove() on an entity whose row was already deleted by another transaction or by direct SQL; versioned update where the version column changed; @SQLUpdate returning 0 rows; flush of a detached entity after the row was archived.","commonSituations":"Two sessions or services editing the same row; DBA/cleanup jobs deleting rows while the app holds entities in memory; retry logic re-running a delete; replica lag causing reads of rows already gone from the primary.","solutions":["Before deleting/updating, verify presence with a fresh find()/findById and skip gracefully if gone","Catch StaleStateException, clear the persistence context, reload, and decide: retry, merge, or report a conflict","If rows can legitimately vanish, mark the operation with @Expectation(none) or use a direct bulk JPQL delete with no count check"],"exampleFix":"// before\nPerson p = em.getReference(Person.class, id);\nem.remove(p); em.flush(); // StaleStateException: row already gone\n\n// after\nPerson p = em.find(Person.class, id); // null-safe hit check\nif (p != null) { em.remove(p); em.flush(); }\n// else: treat as already deleted - no error","handlingStrategy":"try-catch","validationCode":"if (em.find(Person.class, id) == null) {\n    // row already gone - skip the delete instead of flushing into StaleStateException\n    return;\n}","typeGuard":null,"tryCatchPattern":"try {\n    em.remove(target);\n    em.flush();\n} catch (org.hibernate.StaleStateException e) {\n    em.clear();\n    if (em.find(Person.class, id) == null) {\n        // concurrent delete won - treat as success or report conflict\n    } else {\n        // version conflict - reload, reapply, retry once\n    }\n}","preventionTips":["Check existence with find() before remove() on entities that others may delete","Avoid holding detached entities across long user think-time","Investigate out-of-band SQL jobs that delete rows the ORM still references"],"tags":["optimistic-locking","concurrency","flush","delete"],"backgroundTag":"optimistic-lock-stale-state","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}