{"record":{"id":"f48514c95e82efd1","repo":"hibernate/hibernate-orm","slug":"row-was-already-updated-or-deleted-by-another-tran-f48514","errorCode":null,"errorMessage":"Row was already updated or deleted by another transaction","messagePattern":"Row was already updated or deleted by another transaction","errorType":"exception","errorClass":"StaleObjectStateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractSelectLockingStrategy.java","lineNumber":108,"sourceCode":"\t\t\ttry {\n\t\t\t\tlockable.getIdentifierType().nullSafeSet( preparedStatement, id, 1, session );\n\t\t\t\tif ( lockable.isVersioned() ) {\n\t\t\t\t\tlockable.getVersionType().nullSafeSet(\n\t\t\t\t\t\t\tpreparedStatement,\n\t\t\t\t\t\t\tversion,\n\t\t\t\t\t\t\tlockable.getIdentifierType().getColumnSpan( factory.getRuntimeMetamodels() ) + 1,\n\t\t\t\t\t\t\tsession\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tfinal var resultSet = jdbcCoordinator.getResultSetReturn().extract( preparedStatement, sql );\n\t\t\t\ttry {\n\t\t\t\t\tif ( !resultSet.next() ) {\n\t\t\t\t\t\tfinal var statistics = factory.getStatistics();\n\t\t\t\t\t\tif ( statistics.isStatisticsEnabled() ) {\n\t\t\t\t\t\t\tstatistics.optimisticFailure( lockable.getEntityName() );\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthrow new StaleObjectStateException( lockable.getEntityName(), id );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfinally {\n\t\t\t\t\tjdbcCoordinator.getLogicalConnection().getResourceRegistry().release( resultSet, preparedStatement );\n\t\t\t\t}\n\t\t\t}\n\t\t\tfinally {\n\t\t\t\tjdbcCoordinator.getLogicalConnection().getResourceRegistry().release( preparedStatement );\n\t\t\t\tjdbcCoordinator.afterStatementExecution();\n\t\t\t}\n\t\t}\n\t\tcatch ( SQLException sqle ) {\n\t\t\tthrow convertException( object, jdbcException( id, session, sqle, sql ) );\n\t\t}\n\t}\n\n\tprivate JDBCException jdbcException(Object id, SharedSessionContractImplementor session, SQLException sqle, String sql) {\n\t\treturn session.getJdbcServices().getSqlExceptionHelper()","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractSelectLockingStrategy.java#L90-L126","documentation":"Select-based pessimistic locking (AbstractSelectLockingStrategy) runs a 'select ... for update'-style statement and expects exactly one row back. When resultSet.next() returns false the row was not visible - deleted by a concurrent transaction, never committed, or the id/version simply does not exist - and Hibernate throws StaleObjectStateException, whose built-in message is 'Row was already updated or deleted by another transaction'. Statistics (optimisticFailure) are bumped when enabled.","triggerScenarios":"session.lock(entity, LockMode.PESSIMISTIC_WRITE) on an entity whose row another transaction has deleted or not yet committed; locking a detached entity whose id was removed; read-uncommitted/read-committed isolation hiding an uncommitted insert; wrong id passed after a merge. The select returns an empty result set and the exception carries entityName and id.","commonSituations":"Locking entities referenced by stale foreign keys (row already gone); concurrent delete flows racing lock flows; tests that lock rolled-back fixtures; isolation-level differences between environments making rows invisible.","solutions":["Catch StaleObjectStateException and treat it as 'entity no longer exists' - reload and either skip, recreate, or surface a conflict to the user","Verify existence right before locking when deletes are common (or re-check after the exception using session.find)","Coordinate delete flows and lock flows so they cannot interleave on the same rows","Log the entity name and identifier from the exception to identify which row vanished"],"exampleFix":"// before\nsession.lock(orderLine, LockMode.PESSIMISTIC_WRITE);\n\n// after\ntry {\n    session.lock(orderLine, LockMode.PESSIMISTIC_WRITE);\n}\ncatch (StaleObjectStateException e) {\n    OrderLine fresh = session.find(OrderLine.class, orderLine.getId());\n    if (fresh == null) {\n        // row deleted concurrently: skip or re-create\n    }\n}","handlingStrategy":"try-catch","validationCode":"// Optional pre-check when deletes are expected\nObject fresh = session.find(entityClass, id);\nif (fresh == null) {\n    // skip locking - row already gone\n}","typeGuard":null,"tryCatchPattern":"try {\n    session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(entity);\n}\ncatch (StaleObjectStateException e) {\n    // row not visible: deleted or never committed - verify and handle\n    if (session.find(entityClass, id) == null) {\n        // treat as deleted: skip or compensate\n    }\n    else {\n        // visible now (uncommitted earlier): retry the lock once\n    }\n}","preventionTips":["Re-verify existence after a failed select-based lock instead of assuming corruption","Coordinate delete flows and lock flows so they do not race on the same rows","Include entity name + id from the exception in logs to trace which row disappeared"],"tags":["hibernate","locking","stale-state","concurrency","select-for-update"],"backgroundTag":"stale-object-state","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}