Fix MongoServerError: The update operation document must contain atomic operators

intermediate🍃 MongoDB2026-07-04| MongoDB 4.2+, MongoDB 5.0+, Node.js với mongoose hoặc mongodb driver, cũng tái hiện được qua mongosh

Error Message

MongoServerError: The update operation document must contain atomic operators
#mongodb#update#aggregation-pipeline#atomic

Lỗi gặp phải

MongoServerError: The update operation document must contain atomic operators

Lỗi này xảy ra khi bạn gọi updateOne, updateMany, hoặc findOneAndUpdate và MongoDB từ chối update document. Bạn hoặc là truyền vào một plain object không có operator, hoặc thử dùng pipeline update nhưng sai cú pháp.

Nguyên nhân gốc rễ

Tham số update của MongoDB phải có một trong hai dạng sau:

  • Một update document thông thường — phải bắt đầu bằng atomic operator như $set, $unset, $push, $inc, v.v.
  • Một aggregation pipeline — phải là một mảng các pipeline stage như [{ $set: {} }] hoặc [{ $unset: [] }].

Truyền vào MongoDB một plain object không có operator prefix là bế tắc. Nó không thể biết bạn muốn thay thế toàn bộ document (đó là việc của replaceOne) hay chỉ muốn partial atomic update. Vì vậy nó throw lỗi.

Các tình huống thường gặp:

  • Quên bọc $set quanh các field muốn update
  • Truyền pipeline stage dưới dạng plain object thay vì mảng
  • Dùng aggregation operator như $expr hoặc $lookup vốn không phải là pipeline stage hợp lệ trong update
  • Copy một filter document rồi vô tình dùng nó làm tham số update

Cách sửa 1 — Thêm wrapper $set (cách sửa phổ biến nhất)

Chín trong mười trường hợp, bạn chỉ đơn giản là quên operator.

// Sai — plain object, không có atomic operator
await db.collection('users').updateOne(
  { _id: userId },
  { name: 'Alice', role: 'admin' }   // ← gây ra lỗi
);

// Đúng — bọc bằng $set
await db.collection('users').updateOne(
  { _id: userId },
  { $set: { name: 'Alice', role: 'admin' } }
);

Tương tự trong Mongoose:

// Sai
await User.updateOne({ _id: id }, { status: 'active' });

// Đúng
await User.updateOne({ _id: id }, { $set: { status: 'active' } });

Cách sửa 2 — Dùng đúng cú pháp pipeline update (mảng, không phải object)

MongoDB 4.2 đã thêm tính năng aggregation pipeline update — và pipeline bắt buộc phải là một mảng. Điều này làm nhiều người vấp phải liên tục.

// Sai — pipeline stage được truyền dưới dạng object
await db.collection('orders').updateOne(
  { _id: orderId },
  { $set: { total: { $add: ['$subtotal', '$tax'] } } }  // $add sẽ không tính toán ở đây
);

// Đúng — bọc trong dấu ngoặc mảng
await db.collection('orders').updateOne(
  { _id: orderId },
  [
    { $set: { total: { $add: ['$subtotal', '$tax'] } } }
  ]  // ← mảng là bắt buộc
);

Dạng mảng mở ra khả năng mà update document thông thường không làm được: tham chiếu đến giá trị field hiện có bằng tiền tố $ bên trong $set. Đó là cách $add: ['$subtotal', '$tax'] hoạt động — nó đọc hai field trực tiếp từ document và tính ra field thứ ba.

Cách sửa 3 — Các stage hợp lệ cho pipeline update

Chỉ có ba stage được phép bên trong một update pipeline:

  • $set (alias: $addFields)
  • $unset
  • $replaceWith (alias: $replaceRoot)

Chỉ vậy thôi. Thử dùng $match, $group, hoặc $lookup bên trong update pipeline và bạn sẽ gặp lỗi này — hoặc một lỗi khác. Những stage đó thuộc về aggregation đọc dữ liệu, không phải ghi dữ liệu.

// Sai — $group không phải là update stage hợp lệ
await db.collection('logs').updateMany(
  { userId },
  [
    { $group: { _id: '$type', count: { $sum: 1 } } }  // ← không hợp lệ ở đây
  ]
);

// Đúng — dùng $set để tính toán và lưu trữ các field dẫn xuất
await db.collection('logs').updateMany(
  { userId },
  [
    { $set: { processedAt: '$$NOW', flagged: { $gt: ['$errorCount', 10] } } }
  ]
);

Cách sửa 4 — Phân biệt update và replace

Đôi khi bạn thực sự muốn thay thế toàn bộ document, không phải merge vào nó. Hãy dùng replaceOne cho trường hợp đó. Không cần operator nào cả.

// Thay thế toàn bộ — replaceOne chấp nhận plain document
await db.collection('users').replaceOne(
  { _id: userId },
  { name: 'Alice', role: 'admin', updatedAt: new Date() }  // không cần $set
);

// Partial update — giữ nguyên các field hiện có, chỉ thay đổi những gì bạn chỉ định
await db.collection('users').updateOne(
  { _id: userId },
  { $set: { name: 'Alice', role: 'admin', updatedAt: new Date() } }
);

Các ví dụ pipeline update thực tế hoạt động được

// Tính tổng từ hai field hiện có ngay tại thời điểm ghi
await db.collection('invoices').updateOne(
  { _id: invoiceId },
  [
    {
      $set: {
        total: { $multiply: ['$quantity', '$unitPrice'] },
        updatedAt: '$$NOW'
      }
    }
  ]
);

// Đổi field status theo điều kiện — chỉ khi còn là 'pending'
await db.collection('tasks').updateMany(
  { dueDate: { $lt: new Date() } },
  [
    {
      $set: {
        status: {
          $cond: {
            if: { $eq: ['$status', 'pending'] },
            then: 'overdue',
            else: '$status'
          }
        }
      }
    }
  ]
);

// Xóa một field cũ khỏi toàn bộ document trong collection
await db.collection('sessions').updateMany(
  {},
  [
    { $unset: 'legacyToken' }
  ]
);

Xác nhận đã sửa thành công

Kiểm tra matchedCountmodifiedCount sau khi gọi. Cả hai phải bằng 1 (hoặc hơn, với updateMany) nếu update thành công.

const result = await db.collection('users').updateOne(
  { _id: userId },
  { $set: { name: 'Alice' } }
);

console.log(result.matchedCount);   // 1 — tìm thấy document
console.log(result.modifiedCount);  // 1 — đã thực sự thay đổi

// Kiểm tra lại giá trị đã lưu
const doc = await db.collection('users').findOne({ _id: userId });
console.log(doc.name);  // 'Alice'

Trong mongosh, chạy trực tiếp để xem phản hồi thô:

db.users.updateOne(
  { _id: ObjectId('...') },
  { $set: { name: 'Alice' } }
)
// Kết quả mong đợi: { acknowledged: true, matchedCount: 1, modifiedCount: 1 }

Phòng tránh

  • Đặt ra quy tắc chung cho cả team và tuân theo: $set cho partial update, replaceOne cho thay thế toàn bộ. Không bao giờ truyền plain document vào updateOne.
  • Với các dự án TypeScript dùng Node.js driver, UpdateFilter<T> bắt buộc phải có operator key ở cấp độ kiểu. Bật strict mode và bạn sẽ phát hiện lỗi này lúc compile thay vì trong production log.
  • Khi viết pipeline update, hãy gõ dấu ngoặc mở [ trước bất cứ thứ gì. Một thói quen nhỏ này giúp tránh hoàn toàn lỗi nhầm lẫn giữa object và mảng.
  • Strict mode của Mongoose sẽ gắn cờ các phép gán field trực tiếp trước khi chúng đến được database. Đáng để bật nếu bạn chưa dùng.

Related Error Notes