{"record":{"id":"77b9ad814d94f659","repo":"hibernate/hibernate-orm","slug":"row-was-already-updated-or-deleted-by-another-tran-77b9ad","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/persister/entity/mutation/MergeCoordinatorTemporal.java","lineNumber":79,"sourceCode":"\t\t\tSharedSessionContractImplementor session) {\n\t\tif ( entityPersister()\n\t\t\t\t.excludedFromTemporalVersioning( dirtyAttributeIndexes, hasDirtyCollection ) ) {\n\t\t\treturn versionUpdateDelegate.update(\n\t\t\t\t\tentity,\n\t\t\t\t\tid,\n\t\t\t\t\trowId,\n\t\t\t\t\tvalues,\n\t\t\t\t\toldVersion,\n\t\t\t\t\tincomingOldValues,\n\t\t\t\t\tdirtyAttributeIndexes,\n\t\t\t\t\thasDirtyCollection,\n\t\t\t\t\tsession\n\t\t\t);\n\t\t}\n\t\telse {\n\t\t\tfinal boolean rowEnded = performRowEndUpdate( entity, id, rowId, oldVersion, session );\n\t\t\tif ( !rowEnded && currentRowExists( id, session ) ) {\n\t\t\t\tthrow new StaleObjectStateException( entityPersister().getEntityName(), id );\n\t\t\t}\n\t\t\treturn entityPersister().getInsertCoordinator().insert( entity, id, values, session );\n\t\t}\n\t}\n\n\tboolean performRowEndUpdate(\n\t\t\tObject entity,\n\t\t\tObject id,\n\t\t\tObject rowId,\n\t\t\tObject oldVersion,\n\t\t\tSharedSessionContractImplementor session) {\n\t\tclass Result implements OperationResultChecker {\n\t\t\tprivate boolean updated;\n\t\t\t@Override\n\t\t\tpublic boolean checkResult(PreparedStatementDetails statementDetails, int affectedRowCount, int batchPosition) {\n\t\t\t\tupdated = affectedRowCount > 0;\n\t\t\t\treturn !updated\n\t\t\t\t\t|| resultCheck( id, statementDetails, affectedRowCount, batchPosition );","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/MergeCoordinatorTemporal.java#L61-L97","documentation":"MergeCoordinatorTemporal handles merge for entities using temporal (system) versioning: it performs a 'row end' update on the old versioned row, then checks the row state. If the row-end update affected nothing but the row still exists, the row changed between being read and being merged, so a StaleObjectStateException is thrown for the entity and id - a classic optimistic-lock conflict surfacing during merge.","triggerScenarios":"session.merge(detachedEntity) on a temporally-versioned entity while another transaction updates or deletes the same row; long edit sessions where the detached state was read long before the merge; concurrent batch jobs upserting the same rows.","commonSituations":"Two users editing the same record in a web app; background schedulers racing user saves; retries after timeouts where the first request actually committed.","solutions":["Catch StaleObjectStateException (JPA surfaces it as OptimisticLockException) around merge, reload fresh state, reapply the user's changes, and retry","Shorten the read-to-merge window: carry a version/row-start token through the UI and reject stale edits early","For hot entities, serialize edits with a pessimistic lock (SELECT ... FOR UPDATE) instead of retrying","If delete-races are expected, check existence explicitly and handle the 'row gone' outcome instead of merging"],"exampleFix":"// before: merge with no conflict handling\nsession.merge(detachedOrder);\n\n// after: retry on stale state\nfor (int attempt = 0; attempt < 3; attempt++) {\n    try {\n        session.merge(detachedOrder);\n        break;\n    }\n    catch (StaleObjectStateException e) { // OptimisticLockException in JPA\n        session.clear();\n        detachedOrder = reloadAndReapplyChanges(session, detachedOrder);\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    session.merge(detached);\n    tx.commit();\n} catch (StaleObjectStateException e) { // surfaced as OptimisticLockException in JPA\n    session.clear();\n    detached = reloadAndReapplyChanges(session, detached);\n    // retry merge with fresh state\n}","preventionTips":["Carry a version/row-token through long edit flows and reject stale edits before merge","Wrap merge of concurrently-edited entities in a bounded retry loop","Log entity + id on StaleObjectStateException to identify contended rows"],"tags":["hibernate","orm","merge","optimistic-locking","stale-object","concurrency"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}