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$setmà 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ùngObject.assign(doc, req.body)trước khi gọi.save(). Nếureq.bodychứ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_idra 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.

