Sửa lỗi MongoDB Update: After applying the update, the (immutable) field '_id' was found to have been altered

intermediate🍃 MongoDB2026-05-06| MongoDB 4.0+, Node.js/Python/Go Drivers, Mongoose ODM, môi trường Linux/Docker

Error Message

After applying the update, the (immutable) field '_id' was found to have been altered
#mongodb#update#immutable#_id

Vấn đềTôi đang xây dựng endpoint update cho một Node.js API thì MongoDB bắt đầu từ chối toàn bộ các lần ghi. Payload trông ổn. Query trông ổn. Nhưng lỗi cứ xuất hiện mãi:

WriteError: After applying the update, the (immutable) field '_id' was found to have been altered

Trong MongoDB, _id là định danh duy nhất và vĩnh viễn của một document. Bạn không thể thay đổi nó sau khi tạo — kể cả khi giá trị vẫn giữ nguyên. Phần khó chịu nhất? Rất có thể bạn không hề có ý định chỉnh sửa nó. MongoDB phát hiện trường này trong payload $set của bạn và từ chối toàn bộ thao tác.

Nguyên nhân thường gặpChín trong mười trường hợp, đây không phải cố ý. Lỗi thường len lỏi vào qua một trong những pattern sau:

  • Spread req.body trực tiếp: Client gửi lên một object đầy đủ — bao gồm cả _id — và bạn truyền thẳng vào $set mà không lọc trước.- Hàm update dùng chung: Một utility function nhận vào toàn bộ document rồi ghi lại vào MongoDB mà không loại bỏ các trường metadata.- Mongoose + Object.assign(): Bạn fetch một document, sau đó dùng Object.assign(doc, req.body) trước khi gọi .save(). Nếu req.body chứa _id, trạng thái nội bộ của Mongoose sẽ bị hỏng.## Cách sửa### 1. Destructure để loại bỏ _id (Cách tốt nhất)Destructuring trong ES6 giải quyết vấn đề này chỉ bằng một dòng. Tách _id ra riêng và để phần còn lại vào payload update:
// Lỗi — truyền _id vào $set
const updateData = req.body;
await db.collection('users').updateOne(
  { _id: userId },
  { $set: updateData }
);

// Đã sửa — _id không bao giờ đến được $set
const { _id, ...payload } = req.body;
await db.collection('users').updateOne(
  { _id: userId },
  { $set: payload }
);

Cách này rõ ràng, dễ đọc và hoạt động trên mọi phiên bản Node.js từ ES2018 trở đi.

2. Xóa key thủ côngNếu destructuring không phù hợp — chẳng hạn bạn đang ở trong một middleware thao tác trực tiếp trên object đã có — hãy xóa key đó trực tiếp:

const updateData = { ...req.body };
delete updateData._id;

db.collection('products').updateOne({ _id: productId }, { $set: updateData });

Hãy chắc chắn spread thành object mới trước ({ ...req.body }). Việc mutate trực tiếp req.body là thói quen xấu, dễ gây ra các bug khó phát hiện ở các bước khác trong vòng đời request.

3. Mongoose: Dùng findByIdAndUpdate thay vì save()Với Mongoose, cách an toàn nhất là bỏ qua hoàn toàn Object.assign():

// Rủi ro — req.body._id có thể làm hỏng bộ theo dõi nội bộ của document
const user = await User.findById(id);
Object.assign(user, req.body);
await user.save();

// An toàn — Mongoose xử lý _id đúng cách ở đây
const { _id, ...updateFields } = req.body;
await User.findByIdAndUpdate(id, updateFields, { new: true });

Tùy chọn { new: true } trả về document sau khi cập nhật thay vì document gốc — tiện lợi khi bạn cần gửi kết quả về cho client ngay lập tức.

Xác nhận đã sửa thành công- Kiểm tra log: Lỗi WriteError phải biến mất. Nếu vẫn còn, có một đường code khác đang truyền _id vào.- Log payload: Thêm console.log(payload) ngay trước lệnh gọi database và xác nhận _id không còn trong object.- Kiểm tra document: Mở MongoDB Compass hoặc Atlas và xác minh document đã được cập nhật đúng — các giá trị đã thay đổi, _id vẫn giữ nguyên.## Ngăn lỗi tái diễn- Allowlist các trường: Thay vì chặn _id, hãy chỉ định rõ những trường bạn chấp nhận — ví dụ: const { name, email, role } = req.body. Cách này đồng thời ngăn chặn lỗ hổng Mass Assignment.- Người dùng Lodash: _.omit(data, ['_id', '__v']) loại bỏ cả Mongo ID lẫn version key của Mongoose chỉ trong một lần gọi.- Thói quen thiết kế API: Đặt _id ở URL path (PUT /api/users/:id), đừng bao giờ để trong request body. Frontend không nên gửi nó, và backend không nên chấp nhận nó.

Related Error Notes