{"record":{"id":"20e1899bc80d6fa3","repo":"yiisoft/yii2","slug":"the-object-being-updated-is-outdated","errorCode":null,"errorMessage":"The object being updated is outdated.","messagePattern":"The object being updated is outdated\\.","errorType":"exception","errorClass":"yii\\db\\StaleObjectException","httpStatus":null,"severity":"error","filePath":"framework/db/BaseActiveRecord.php","lineNumber":832,"sourceCode":"            return false;\n        }\n        $values = $this->getDirtyAttributes($attributes);\n        if (empty($values)) {\n            $this->afterSave(false, $values);\n            return 0;\n        }\n        $condition = $this->getOldPrimaryKey(true);\n        $lock = $this->optimisticLock();\n        if ($lock !== null) {\n            $values[$lock] = $this->$lock + 1;\n            $condition[$lock] = $this->$lock;\n        }\n        // We do not check the return value of updateAll() because it's possible\n        // that the UPDATE statement doesn't change anything and thus returns 0.\n        $rows = static::updateAll($values, $condition);\n\n        if ($lock !== null && !$rows) {\n            throw new StaleObjectException('The object being updated is outdated.');\n        }\n\n        // using null as an array offset is deprecated in PHP `8.5`\n        if ($lock !== null && isset($values[$lock])) {\n            $this->$lock = $values[$lock];\n        }\n\n        $changedAttributes = [];\n        foreach ($values as $name => $value) {\n            $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;\n            $this->_oldAttributes[$name] = $value;\n        }\n        $this->afterSave(false, $changedAttributes);\n\n        return $rows;\n    }\n\n    /**","sourceCodeStart":814,"sourceCodeEnd":850,"githubUrl":"https://github.com/yiisoft/yii2/blob/66f00d18a29b520f85e8e8f1e32d1e7e7b556cac/framework/db/BaseActiveRecord.php#L814-L850","documentation":"BaseActiveRecord::updateInternal() adds optimistic locking to every UPDATE: the lock column is incremented in the SET clause, and the model's current lock value is added to the WHERE clause. If updateAll() then reports 0 affected rows, the stored version no longer matches the loaded model — someone else saved the record in between — and StaleObjectException is thrown.","triggerScenarios":"$model->save() (update path) on a record whose optimisticLock() column changed after the model was loaded; concurrent edits from two tabs/sessions; earlier updateAll() writes that modified the version column without incrementing it, causing permanent drift.","commonSituations":"Long-lived edit forms in admin panels; race between parallel API PATCH requests; batch jobs touching the same rows as interactive users; version column managed inconsistently across write paths.","solutions":["Catch StaleObjectException around save(), call refresh(), re-apply the user's changes onto the fresh record, and retry once","Present a conflict message / 409 response instead of silently losing one side's changes","Ensure every write path increments the lock column (avoid raw updateAll on locked tables)","Drop the optimisticLock() override for tables where last-write-wins is acceptable"],"exampleFix":"// before\nif (!$model->save()) { /* only validation errors handled */ } // StaleObjectException escapes\n\n// after\nuse yii\\db\\StaleObjectException;\n\ntry {\n    $model->save();\n} catch (StaleObjectException $e) {\n    $model->refresh();\n    $model->setAttributes($submittedAttributes);\n    $model->save(); // or surface a conflict to the user\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"use yii\\db\\StaleObjectException;\n\n$attempt = 0;\ndo {\n    try {\n        $model->save();\n        break;\n    } catch (StaleObjectException $e) {\n        $model->refresh();                      // reload current version\n        $model->setAttributes($submittedAttrs); // re-apply user changes\n        $attempt++;\n    }\n} while ($attempt < 2); // one retry, then surface conflict","preventionTips":["Catch StaleObjectException around every save() on optimistic-locked models","Refresh and re-apply instead of retrying the stale instance blindly","Keep write windows short: load, save, done — never hold models across user think-time"],"tags":["optimistic-locking","concurrency","update","stale-object","conflict"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"66f00d18a29b520f85e8e8f1e32d1e7e7b556cac","analyzedAt":"2026-08-17T05:17:23.470Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}