{"record":{"id":"1e939a10406342f2","repo":"hibernate/hibernate-orm","slug":"row-was-already-updated-or-deleted-by-another-tran","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/AbstractPessimisticUpdateLockingStrategy.java","lineNumber":89,"sourceCode":"\t\t\t\tversionType.nullSafeSet( preparedStatement, version, 1, session );\n\t\t\t\tint offset = 2;\n\n\t\t\t\tidentifierType.nullSafeSet( preparedStatement, id, offset, session );\n\t\t\t\toffset += identifierType.getColumnSpan( factory.getRuntimeMetamodels() );\n\n\t\t\t\tif ( lockable.isVersioned() ) {\n\t\t\t\t\tversionType.nullSafeSet( preparedStatement, version, offset, session );\n\t\t\t\t}\n\n\t\t\t\tfinal int affected = jdbcCoordinator.getResultSetReturn().executeUpdate( preparedStatement, sql );\n\t\t\t\t// todo:  should this instead check for exactly one row modified?\n\t\t\t\tif ( affected < 0 ) {\n\t\t\t\t\tfinal var statistics = factory.getStatistics();\n\t\t\t\t\tfinal String entityName = lockable.getEntityName();\n\t\t\t\t\tif ( statistics.isStatisticsEnabled() ) {\n\t\t\t\t\t\tstatistics.optimisticFailure( entityName );\n\t\t\t\t\t}\n\t\t\t\t\tthrow new StaleObjectStateException( entityName, id );\n\t\t\t\t}\n\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 e ) {\n\t\t\tthrow session.getJdbcServices().getSqlExceptionHelper().convert(\n\t\t\t\t\te,\n\t\t\t\t\t\"could not lock: \" + infoString( lockable, id, session.getFactory() ),\n\t\t\t\t\tsql\n\t\t\t);\n\t\t}\n\t}\n\n\tprotected String generateLockString() {","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/dialect/lock/AbstractPessimisticUpdateLockingStrategy.java#L71-L107","documentation":"Update-based pessimistic locking issues UPDATE ... set version=? where id=? and version=?; the constructor-supplied row must still exist with the expected version. When executeUpdate reports fewer than 0 affected rows, Hibernate throws StaleObjectStateException whose message is 'Row was already updated or deleted by another transaction' (built into StaleObjectStateException itself). So even though you asked for a pessimistic lock, the failure mode is a stale-state conflict detected by the version predicate.","triggerScenarios":"Two transactions lock the same unversioned-check row via update locking: the first commits or deletes, then the second's locking UPDATE matches 0 rows (driver returns a negative count for 'no match') and the exception is thrown with the entity name and id. Also possible when the row was hard-deleted between read and lock, or the version value passed is stale.","commonSituations":"Race conditions between a background job and user requests updating the same row; delete-then-lock sequences in the same transaction; retrying operations after a long think time while another request modified the entity.","solutions":["Catch StaleObjectStateException, refresh the entity (session.refresh or re-load) and retry the business operation","Shorten the window between loading and locking (lock early at the start of the transaction)","Ensure the entity actually carries a version column so the predicate is meaningful, and never mix stale detached versions with fresh locks","For hot rows, consider a dedicated lock table or database advisory locks instead of row updates"],"exampleFix":"// before\nsession.lock(person, LockMode.PESSIMISTIC_WRITE);\nprocess(person);\n\n// after\ntry {\n    session.lock(person, LockMode.PESSIMISTIC_WRITE);\n    process(person);\n}\ncatch (StaleObjectStateException e) {\n    session.refresh(person); // reload current state\n    // re-apply or report the conflict\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(person);\n}\ncatch (StaleObjectStateException e) { // org.hibernate\n    // row updated/deleted by a concurrent transaction: reload and re-decide\n    session.refresh(person);\n    // re-apply the business operation or report a conflict\n}","preventionTips":["Lock as early as possible in the transaction to shrink the read-to-lock window","Catch StaleObjectStateException wherever rows are contended and translate it into a user-facing conflict message","Do not reuse detached entities' version values for later locks - always lock managed, freshly loaded instances"],"tags":["hibernate","locking","stale-state","concurrency","pessimistic-lock"],"backgroundTag":"stale-object-state","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}