MongoServerError: The update operation document must contain atomic operators の修正方法

intermediate🍃 MongoDB2026-07-04| MongoDB 4.2+、MongoDB 5.0+、mongoose または mongodb ドライバーを使用した Node.js、mongosh でも再現可能

Error Message

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

エラーの内容

MongoServerError: The update operation document must contain atomic operators

このエラーは、updateOneupdateMany、または findOneAndUpdate を呼び出した際に MongoDB がアップデートドキュメントを拒否したときに発生します。オペレーターを含まないプレーンなオブジェクトを渡したか、パイプライン更新を試みたが構文が間違っていた場合に起こります。

根本原因

MongoDB の更新引数は、次の2つの形式のいずれかでなければなりません:

  • 通常の更新ドキュメント$set$unset$push$inc などのアトミックオペレーターで始まる必要があります。
  • 集計パイプライン[{ $set: {} }][{ $unset: [] }] のようなパイプラインステージの配列でなければなりません。

オペレータープレフィックスのないプレーンなオブジェクトを MongoDB に渡すと処理が止まります。完全な置き換え(それは replaceOne の役割)を意図しているのか、部分的なアトミック更新を意図しているのか判断できないため、エラーをスローします。

よくある原因:

  • 更新したいフィールドを $set で囲み忘れる
  • パイプラインステージを配列ではなくプレーンなオブジェクトとして渡す
  • 更新パイプラインステージとして有効でない $expr$lookup などの集計オペレーターを使用する
  • フィルタードキュメントをコピーして誤って更新引数として使用してしまう

修正方法1 — $set でラップする(最もよくある修正)

ほとんどの場合、オペレーターを書き忘れているだけです。

// 誤り — プレーンなオブジェクトで、アトミックオペレーターなし
await db.collection('users').updateOne(
  { _id: userId },
  { name: 'Alice', role: 'admin' }   // ← エラーを引き起こす
);

// 修正 — $set でラップする
await db.collection('users').updateOne(
  { _id: userId },
  { $set: { name: 'Alice', role: 'admin' } }
);

Mongoose でも同じ問題が起きます:

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

// 修正
await User.updateOne({ _id: id }, { $set: { status: 'active' } });

修正方法2 — 正しいパイプライン更新の構文を使う(オブジェクトではなく配列)

MongoDB 4.2 で集計パイプライン更新が追加されましたが、パイプラインは配列でなければなりません。これは非常に多くの人がつまずくポイントです。

// 誤り — パイプラインステージをオブジェクトとして渡している
await db.collection('orders').updateOne(
  { _id: orderId },
  { $set: { total: { $add: ['$subtotal', '$tax'] } } }  // $add はここでは計算されない
);

// 修正 — 配列の角括弧でラップする
await db.collection('orders').updateOne(
  { _id: orderId },
  [
    { $set: { total: { $add: ['$subtotal', '$tax'] } } }
  ]  // ← 配列が必要
);

配列形式を使うことで、通常の更新ドキュメントでは不可能なことが実現できます。それは $set 内で $ プレフィックスを使って既存のフィールド値を参照することです。$add: ['$subtotal', '$tax'] が機能するのはそのためです — ドキュメントから2つのフィールドを読み取り、3つ目の値を計算します。

修正方法3 — パイプライン更新で有効なステージを使う

更新パイプライン内で使用できるステージは3つだけです:

  • $set(エイリアス:$addFields
  • $unset
  • $replaceWith(エイリアス:$replaceRoot

以上です。更新パイプライン内で $match$group、または $lookup を使おうとすると、このエラーか別のエラーが発生します。これらのステージは集計の読み取りに属するものであり、書き込みには使えません。

// 誤り — $group は有効な更新ステージではない
await db.collection('logs').updateMany(
  { userId },
  [
    { $group: { _id: '$type', count: { $sum: 1 } } }  // ← ここでは無効
  ]
);

// 修正 — $set を使って派生フィールドを計算して保存する
await db.collection('logs').updateMany(
  { userId },
  [
    { $set: { processedAt: '$$NOW', flagged: { $gt: ['$errorCount', 10] } } }
  ]
);

修正方法4 — 更新と置き換えを区別する

ドキュメントをマージするのではなく、完全に置き換えたい場合もあります。その場合は replaceOne を使います。オペレーターは必要ありません。

// 完全置き換え — replaceOne はプレーンなドキュメントを受け付ける
await db.collection('users').replaceOne(
  { _id: userId },
  { name: 'Alice', role: 'admin', updatedAt: new Date() }  // $set は不要
);

// 部分更新 — 既存フィールドを保持し、指定したフィールドのみ変更する
await db.collection('users').updateOne(
  { _id: userId },
  { $set: { name: 'Alice', role: 'admin', updatedAt: new Date() } }
);

動作するパイプライン更新の実用例

// 書き込み時に既存の2つのフィールドから合計を算出する
await db.collection('invoices').updateOne(
  { _id: invoiceId },
  [
    {
      $set: {
        total: { $multiply: ['$quantity', '$unitPrice'] },
        updatedAt: '$$NOW'
      }
    }
  ]
);

// ステータスフィールドを条件付きで反転する — 'pending' の場合のみ
await db.collection('tasks').updateMany(
  { dueDate: { $lt: new Date() } },
  [
    {
      $set: {
        status: {
          $cond: {
            if: { $eq: ['$status', 'pending'] },
            then: 'overdue',
            else: '$status'
          }
        }
      }
    }
  ]
);

// コレクション内のすべてのドキュメントからレガシーフィールドを削除する
await db.collection('sessions').updateMany(
  {},
  [
    { $unset: 'legacyToken' }
  ]
);

修正の確認

呼び出し後に matchedCountmodifiedCount を確認します。更新が成功していれば、どちらも 1 以上(updateMany の場合)になるはずです。

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

console.log(result.matchedCount);   // 1 — ドキュメントが見つかった
console.log(result.modifiedCount);  // 1 — 実際に変更された

// 保存された値をスポットチェックする
const doc = await db.collection('users').findOne({ _id: userId });
console.log(doc.name);  // 'Alice'

mongosh では、インタラクティブに実行して生のレスポンスを確認できます:

db.users.updateOne(
  { _id: ObjectId('...') },
  { $set: { name: 'Alice' } }
)
// 期待される結果: { acknowledged: true, matchedCount: 1, modifiedCount: 1 }

予防策

  • チームのルールを決めて守りましょう:部分更新には $set、完全置き換えには replaceOne。プレーンなドキュメントを updateOne に渡さないようにします。
  • Node.js ドライバーを使用した TypeScript プロジェクトでは、UpdateFilter<T> が型レベルでオペレーターキーを強制します。strict モードを有効にすれば、本番ログで気づくのではなくコンパイル時に問題を検出できます。
  • パイプライン更新を書く際は、最初に開き [ ブラケットを入力しましょう。この習慣一つで、オブジェクトと配列を間違えることがなくなります。
  • Mongoose の strict モードは、データベースに到達する前に裸のフィールド代入を検出します。まだ使っていない場合は有効化する価値があります。

Related Error Notes