問題の概要Node.js APIの更新エンドポイントを構築していたとき、MongoDBが突然すべての書き込みを拒否し始めました。ペイロードは問題なく見えました。クエリも問題なく見えました。しかしエラーは容赦なく続きました:
WriteError: After applying the update, the (immutable) field '_id' was found to have been altered
MongoDBでは、_idはドキュメントの永続的な一意識別子です。作成後に変更することは決してできません — 同じ値であっても不可です。厄介なのは、そもそも触るつもりがなかった場合がほとんどだという点です。MongoDBは$setペイロードにそのフィールドを見つけると、操作全体を拒否します。
よくある発生パターンほとんどの場合、これは意図的なものではありません。以下のいずれかのパターンで紛れ込みます:
- **req.bodyをそのまま展開する:**クライアントが
_idを含むフルオブジェクトを送信し、フィルタリングせずに$setにそのまま渡してしまう。- **汎用的な更新ヘルパー:**共通のユーティリティ関数がフルドキュメントを受け取り、メタデータフィールドを除去せずにMongoDBに書き戻す。- **Mongoose + Object.assign():**ドキュメントを取得した後、.save()を呼び出す前にObject.assign(doc, req.body)を使用する。req.bodyに_idが含まれていると、Mongooseの内部状態が破損する。## 修正方法### 1. _idを分割代入で除外する(最善の方法)ES6の分割代入を使えば1行で解決できます。_idを単独で取り出し、残りを更新ペイロードに使います:
// 問題あり — $setに_idが渡される
const updateData = req.body;
await db.collection('users').updateOne(
{ _id: userId },
{ $set: updateData }
);
// 修正済み — _idが$setに渡されない
const { _id, ...payload } = req.body;
await db.collection('users').updateOne(
{ _id: userId },
{ $set: payload }
);
これはシンプルで読みやすく、ES2018以降のすべてのNode.jsバージョンで動作します。
2. キーを手動で削除する分割代入が適さない場合 — たとえば既存オブジェクトを変更するミドルウェアの中など — キーを直接削除する方法もあります:
const updateData = { ...req.body };
delete updateData._id;
db.collection('products').updateOne({ _id: productId }, { $set: updateData });
必ず最初に新しいオブジェクトにスプレッドしてください({ ...req.body })。req.body自体を変更するのは、リクエストのライフサイクルの他の部分で微妙なバグを引き起こす悪い習慣です。
3. Mongoose:save()の代わりにfindByIdAndUpdateを使うMongooseでは、Object.assign()を完全に避けるのが最も安全な方法です:
// 危険 — req.body._idがドキュメントの内部トラッカーを破損させる可能性がある
const user = await User.findById(id);
Object.assign(user, req.body);
await user.save();
// 安全 — Mongooseがここで_idを正しく処理する
const { _id, ...updateFields } = req.body;
await User.findByIdAndUpdate(id, updateFields, { new: true });
{ new: true }オプションは元のドキュメントではなく更新済みのドキュメントを返します — すぐにクライアントに結果を返す必要がある場合に便利です。

