# instinct: "SQLAlchemyのStaleDataError「expected 1 row, N matched」は競合状態のread-modify-write UPSERTを疑いON CONFLICTで根治"
`sqlalchemy.orm.exc.StaleDataError: UPDATE statement on table 'X' expected to update 1 row(s); 2 were matched` を見たら、**SELECT→add/update→commit の read-modify-write UPSERTの競合状態**を疑う。
**症状の典型**: 短時間(数十秒)にエラーが集中=ユーザーがUIで保存/連打した瞬間。調査時点ではDB重複が0件・ユニーク制約も実在することが多い(純粋なアプリ側ロジックの競合で、DB状態は健全)。
**原因**: ループ内で `.first()` で既存検索→無ければ `db.add()`、最後に一括 `db.commit()` という実装は、①同一リクエスト内に同じキーが複数 ②同時リクエスト で `.first()` が両方「存在しない」と判定し2行INSERTを試み、ユニーク制約と衝突する。
**根治**: PostgreSQLなら `from sqlalchemy.dialects.postgresql import insert as pg_insert` で `INSERT ... ON CONFLICT (key...) DO UPDATE`(UPSERT)に置換。**リクエスト内のキー重複は dict で事前dedup**(しないと `command cannot affect row a second time` エラーになる)。
```python
deduped = {} # 後勝ちでdedup
for item in items: deduped[item.key] = item.value
stmt = pg_insert(Model).values([{...} for k,v in deduped.items()])
stmt = stmt.on_conflict_do_update(index_elements=["a","b","c"], set_={"v": stmt.excluded.v})
db.execute(stmt); db.commit()
```
**Why:** read-modify-writeは原子的でないので並行/重複に弱い。UPSERTはDB側で原子的に解決する。
**検証法**: 実DBで「同一キー重複を含むリクエスト」を流して例外が出ないこと・最後の指定が採用されることを確認(mail-cleaner #22で実証)。
関連: [[instinct-postgres-sequence-drift-after-etl]](ETL系のDB整合)・[[instinct-python-module-edit-needs-restart]](修正後の再起動)
---
(migrated from local memory/instinct_staledata_error_race_upsert.md)
instinct: "SQLAlchemyのStaleDataError「expected 1 row, N matched」は競合状態のread-modify-write UPSERTを疑いON CONFLICTで根治"