MongoDB: ドキュメント更新:updateOneとupdateManyの詳解
最終更新:2026-08-26
ドキュメント更新はMongoDBで最も一般的な書き込み操作の一つです。update修飾子をマスターすることがデータ変更の鍵となります。
本コースでは、updateOne、updateMany、replaceOne、各種更新修飾子($set、$inc、$push、$pull)、upsertの動作、原子性の保証について詳しく解説します。
1. 学習内容
- updateOne / updateMany / replaceOneの主な違い
- $set / $unset / $inc / $mul / $rename フィールド更新
- $push / $pull / $addToSet / $pop 配列更新
- upsertオプション(なければ挿入)
- 更新操作の原子性保証
- 戻り値の解釈(matchedCount / modifiedCount)
2. eコマースプラットフォームの実話
(1) 課題:在庫引当時の同時実行の問題
Aliceはeコマース企業で注文システムを管理しており、在庫更新時に典型的な同時実行の問題に遭遇しました:
// ❌ 反例:先に確認してから更新(競合状態)
app.post('/api/orders', async (req, res) => {
const product = await Product.findOne({ sku: 'PHONE-001' });
if (product.stock <= 0) {
return res.status(400).json({ error: 'Out of stock' });
}
// ⚠️ 同時実行の問題:両方のリクエストが stock=1 を読み取り
await Product.updateOne(
{ sku: 'PHONE-001' },
{ $inc: { stock: -1 } }
);
// 両方のリクエストが正常に引当、結果として在庫が-1になる
});
(2) MongoDBでの原子的更新による解決策
// ✅ 正例:フィルター条件 + 原子操作
app.post('/api/orders', async (req, res) => {
const result = await Product.updateOne(
{ sku: 'PHONE-001', stock: { $gt: 0 } }, // 重要:条件フィルタリング
{ $inc: { stock: -1 } }
);
if (result.modifiedCount === 0) {
return res.status(400).json({ error: 'Out of stock' });
}
// 変更された件数が1の場合のみ成功
});
(3) 効果
| 観点 | 先に確認してから更新 | 原子的更新 |
|---|---|---|
| 同時実行安全性 | ❌ 競合状態 | ✅ 原子操作 |
| パフォーマンス | ⚠️ 2回のクエリ | ⚡ 1回の操作 |
| コードの複雑さ | 高い | 低い |
3. updateOne:単一ドキュメントを更新
概念説明明: updateOneはMongoDBで最もよく使われる更新メソッドです。フィルター条件に基づいて最初のドキュメントを特定し、更新操作を適用します。SQLのUPDATE ... SET ... WHERE ...と似ていますが、MongoDBは更新修飾子($setや$incなど)を使用して何を変更するかを指定し、ドキュメント全体を置き換えるのではありません。この設計により、部分更新がより効率的になります。変更されたフィールドのみが修正され、ドキュメント全体を書き直す必要がありません。
仕組み: updateOneの実行フローは以下の通りです:マッチングフェーズ(フィルターに基づいてドキュメントを特定)→ 更新フェーズ(更新修飾子を適用)→ インデックス更新(インデックスフィールドが変更された場合)→ 書き込み確認。操作全体が単一ドキュメントに対して原子的です。「フィールドの半分だけが更新された」という中間状態はありません。
sequenceDiagram
participant App as アプリケーション
participant Mongo as MongoDB
participant WT as WiredTiger
App->>Mongo: updateOne({ sku: "PHONE-001" }, { $set: { price: 699 } })
Mongo->>Mongo: フィルターをマッチ(インデックス使用)
Mongo->>Mongo: $setを適用
Mongo->>Mongo: インデックス更新が必要かチェック
Mongo->>WT: 変更されたドキュメントを保存
WT-->>Mongo: 確認
Mongo-->>App: { matchedCount: 1, modifiedCount: 1 }
| パラメータ | 型 | 説明 |
|---|---|---|
filter |
ドキュメント | 検索条件(必須) |
update |
ドキュメント | 更新操作(必須、修飾子を含む必要あり) |
options |
ドキュメント | upsert / writeConcernなど(任意) |
| 戻り値フィールド | 意味 | 注意点 |
|---|---|---|
matchedCount |
マッチしたドキュメント数 | 0の可能性あり |
modifiedCount |
実際に変更されたドキュメント数 | 値が同じで変わらない場合は0 |
upsertedCount |
upsertで挿入されたドキュメント数 | upsert: trueの場合のみ1の可能性 |
(1) 基本構文
// === updateOne基本使用法 ===
db.products.updateOne(
{ sku: "PHONE-001" }, // フィルター
{ $set: { price: 699.99 } } // 更新
);
出力:
TEXT 📖 参照専用{ acknowledged: true, matchedCount: 1, // マッチしたドキュメント数 modifiedCount: 1, // 変更されたドキュメント数 upsertedCount: 0, // 挿入されたドキュメント数 upsertedId: null // 挿入された_id }
(2) 戻り値の解釈
const result = await Product.updateOne(
{ sku: 'PHONE-001' },
{ $set: { stock: 50 } }
);
result.acknowledged; // true(書き込み確認済み)
result.matchedCount; // 1(1件マッチ)
result.modifiedCount; // 1(実際に1件変更)
result.upsertedCount; // 0(挿入なし)
出力:
TEXT 📖 参照専用true 1 1 0
| フィールド | 意味 |
|---|---|
matchedCount |
フィルターにマッチしたドキュメント数 |
modifiedCount |
実際に変更されたドキュメント数 |
upsertedCount |
upsertで挿入されたドキュメント数 |
upsertedId |
挿入されたドキュメントの_id |
(3) マッチしない場合の処理
const result = await Product.updateOne(
{ sku: 'NOT_EXIST' },
{ $set: { stock: 0 } }
);
print(result.matchedCount); // 0
print(result.modifiedCount); // 0
// エラーなし、変更なし
出力:
TEXT 📖 参照専用0 0
▶ サンプル 1:updateOneの実践
// === 単一フィールドを変更 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { price: 699.99 } }
);
// === 複数フィールドを変更 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{
$set: {
price: 699.99,
stock: 50,
lastUpdated: new Date()
}
}
);
// === ネストフィールドを更新 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "specs.battery": "5000mAh" } }
);
// === Mongoose相当の記述 ===
const result = await Product.updateOne(
{ sku: 'PHONE-001' },
{ $set: { price: 699.99, lastUpdated: new Date() } }
);
4. updateMany:一括更新
概念説明明: updateManyは条件に一致する全ドキュメントをマッチし、全てに更新操作を一様に適用します。最初のマッチのみを変更するupdateOneとは異なり、updateManyは一度に数万件のドキュメントを更新できます。これが一括変更(全商品割引、一括出品停止、データ修復など)の主要メソッドです。
仕組み: updateManyはまずクエリを実行して条件を満たす全ドキュメントをマッチし、各ドキュメントに更新操作を順次適用します。更新プロセスはトランザクションではありません。途中で失敗した場合、既に更新されたドキュメントはロールバックされません。そのため、一括更新を行う際は、バッチ単位での実行とエラーハンドリングを検討する必要があります。
| 観点 | updateOne | updateMany |
|---|---|---|
| マッチ範囲 | 最初の1件 | 全マッチ |
| 一括操作 | ❌ 単一 | ✅ 一括 |
| トランザクションロールバック | ❌ 非対応 | ❌ 非対応 |
| 用途 | 個別編集 | 一括割引、出品停止、修復 |
| リスク | 低い | 中程度(操作ミスの影響が大きい) |
(1) 基本構文
// === updateMany基本使用法 ===
db.products.updateMany(
{ category: 'Electronics' }, // フィルター(複数マッチ)
{ $set: { discount: 0.1 } } // 更新(一括適用)
);
出力:
TEXT 📖 参照専用{ acknowledged: true, matchedCount: 250, // 250件マッチ modifiedCount: 250, // 250件変更 upsertedCount: 0 }
(2) 一括更新の注意点
// ⚠️ updateManyはトランザクションによるロールバックに対応していない
// 途中で失敗した場合、既に変更された内容はロールバックされない
// ⚠️ 大規模な更新はコレクションをロックする可能性がある
// バッチサイズ制御の使用を推奨:
const BATCH_SIZE = 1000;
let modified = 0;
let lastId = null;
while (true) {
const result = await Product.updateMany(
{
category: 'Electronics',
_id: { $gt: lastId }
},
{ $set: { onSale: true } },
{ limit: BATCH_SIZE } // Mongooseオプション
);
if (result.modifiedCount === 0) break;
modified += result.modifiedCount;
}
▶ サンプル 2:一括更新の実践ガイド
// === 全Electronics商品に10%割引を適用 ===
db.products.updateMany(
{ category: 'Electronics' },
{ $mul: { price: 0.9 } }
);
// === 全ての期限切れ商品を出品停止 ===
db.products.updateMany(
{ expiryDate: { $lt: new Date() } },
{ $set: { isActive: false } }
);
// === 5つ星評価の商品全てにタグを追加 ===
db.products.updateMany(
{ rating: { $gte: 4.8 } },
{ $addToSet: { tags: 'top-rated' } }
);
5. replaceOne:ドキュメント全体を置換
概念説明明: replaceOneとupdateOneの根本的な違いは、updateOneは修飾子で指定されたフィールドのみを更新し、未指定のフィールドを保持することに対し、replaceOneはドキュメントの内容を完全に置き換え、未指定のフィールドは削除されます。これはMongoDBで最も危険な操作の一つであり、誤用はデータ損失につながります。
用途: ドキュメントを完全に書き直す必要がある場合(例:データ移行やドキュメントフォーマットのアップグレード)にのみreplaceOneを使用してください。ほとんどの場合、updateOne + $setで必要なフィールドのみを変更することを推奨します。
| 観点 | updateOne + $set | replaceOne |
|---|---|---|
| 未指定フィールド | ✅ 保持 | ❌ 削除 |
| 原子性 | ✅ 単一ドキュメント原子性 | ✅ 単一ドキュメント原子性 |
| 用途 | 選択フィールドの変更 | ドキュメント全体の書き直し |
| リスク | 低い | 高い(フィールド損失) |
(1) updateOneとの違い
// === updateOne:指定フィールドのみ変更 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { price: 699 } }
);
// 結果:{ _id, sku, title, price: 699, stock, category, ... }(他フィールドを保持)
// === replaceOne:ドキュメント全体を置換 ===
db.products.replaceOne(
{ sku: 'PHONE-001' },
{ sku: 'PHONE-001', title: 'New Phone', price: 799 }
);
// 結果:{ _id, sku, title: 'New Phone', price: 799 }
// ⚠️ 他のフィールド(stock、categoryなど)は全て失われる!
(2) replaceOneの用途
// ✅ 適用:ドキュメントを完全に書き直し
db.users.replaceOne(
{ _id: 'user_001' },
{
_id: 'user_001',
name: 'Alice',
email: 'alice@example.com',
role: 'admin',
updatedAt: new Date()
}
);
// ❌ 非適用:1つのフィールドを変更したいだけ(updateOne + $setを使用)
6. フィールド更新修飾子
概念説明明: 更新修飾子はMongoDB更新操作の核となる構文で、ドキュメントフィールドの変更方法を定義します。SQLのSET field = valueとは異なり、MongoDBは豊富な修飾子セットを提供します。$set(値設定)、$unset(フィールド削除)、$inc(増減)、$mul(乗算)、$rename(名前変更)、$min/$max(条件付き更新)、$currentDate(現在時刻)、$setOnInsert(upsert時のみ設定)などです。
仕組み: 更新修飾子はドキュメントレベルで原子的に適用されます。全修飾子の効果が完全に適用されるか、全く適用されないかのいずれかです。複数の修飾子を組み合わせて使用できます(例:$set + $inc + $currentDate)が、同一フィールドに複数の修飾子を適用することはできません。
graph TB
A[更新修飾子] --> B[フィールド値クラス<br/>$set/$unset/$inc/$mul]
A --> C[フィールド名クラス<br/>$rename]
A --> D[条件更新クラス<br/>$min/$max]
A --> E[時刻関連<br/>$currentDate]
A --> F[upsert専用<br/>$setOnInsert]
style A fill:#cce5ff
| 修飾子 | 機能 | 例 | フィールド作成 |
|---|---|---|---|
$set |
フィールド値を設定 | { $set: { price: 699 } } |
存在しない場合は作成 |
$unset |
フィールドを削除 | { $unset: { discount: "" } } |
存在しない場合は無視 |
$inc |
増減 | { $inc: { stock: -1 } } |
存在しない場合は0から開始 |
$mul |
乗算 | { $mul: { price: 0.9 } } |
存在しない場合は0から開始 |
$rename |
フィールド名を変更 | { $rename: { "stock": "qty" } } |
— |
$min |
小さい方を採用 | { $min: { price: 500 } } |
存在しない場合は作成 |
$max |
大きい方を採用 | { $max: { price: 1000 } } |
存在しない場合は作成 |
$currentDate |
現在時刻を設定 | { $currentDate: { updatedAt: true } } |
存在しない場合は作成 |
$setOnInsert |
upsert時のみ設定 | { $setOnInsert: { createdAt: new Date() } } |
挿入時のみ作成 |
(1) $set フィールド値を設定
// === フィールド設定 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { stock: 50, isActive: true } }
);
// === ネストフィールド設定 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "specs.battery": "5000mAh" } }
);
// === 配列要素設定 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "tags.0": "5g", "tags.1": "amoled" } }
);
(2) $unset:フィールドを削除
// === 単一フィールド削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $unset: { discount: "" } }
);
// === 複数フィールド削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $unset: { discount: "", internalNotes: "" } }
);
// === ネストフィールド削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $unset: { "specs.battery": "" } }
);
(3) $inc 増減
// === 在庫減少 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $inc: { stock: -1 } }
);
// === 閲覧数+1 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $inc: { viewCount: 1 } }
);
// === 複数フィールドの累積スコア ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $inc: { stock: -1, soldCount: 1, viewCount: 1 } }
);
(4) $mul 乗算
// === 10%割引を適用 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $mul: { price: 0.9 } }
);
// === 価格を倍増 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $mul: { price: 2 } }
);
(5) $rename:フィールド名を変更
// === フィールド名変更 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $rename: { "stock": "inventory" } }
);
// stock → inventory
// === ネストフィールド名変更 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $rename: { "specs.battery": "specs.batteryCapacity" } }
);
(6) $min / $max:最小/最大値を採用
// === $min:値が小さい場合のみ更新 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $min: { price: 500 } }
);
// 現在の価格 > 500 の場合、500に変更、それ以外は変更なし
// === $max:値が大きい場合のみ更新 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $max: { price: 1000 } }
);
(7) $currentDate 現在日時を設定
// === 現在時刻を設定 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $currentDate: { lastModified: true } }
);
// === Date型で設定 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $currentDate: { lastModified: { $type: "date" } } }
);
(8) $setOnInsert:upsert時にフィールドを設定
// === upsert時のみデフォルト値を設定 ===
db.products.updateOne(
{ sku: 'NEW-001' },
{
$set: { price: 599 },
$setOnInsert: { createdAt: new Date(), stock: 0 }
},
{ upsert: true }
);
// 挿入時:{ sku: 'NEW-001', price: 599, createdAt: ..., stock: 0 }
// 更新時:{ sku: 'NEW-001', price: 599 }(createdAt、stockは設定されない)
▶ サンプル 3:複合フィールド更新の実践ガイド
// === 支払い成功後に注文ステータスを更新 ===
db.orders.updateOne(
{ _id: orderId },
{
$set: {
status: 'paid',
paidAt: new Date(),
paymentMethod: 'credit_card'
},
$inc: { version: 1 }, // 楽観ロックバージョン番号
$currentDate: { updatedAt: true }
}
);
// === ユーザーログイン後に最終ログイン時刻を更新 ===
db.users.updateOne(
{ _id: userId },
{
$set: { lastLoginAt: new Date(), lastLoginIp: '192.168.1.1' },
$inc: { loginCount: 1 }
}
);
7. 配列更新修飾子
概念説明明: 配列はMongoDBドキュメントで最も柔軟なデータ構造ですが、配列要素の更新は通常フィールドの更新より複雑です。MongoDBは専用の配列修飾子を提供します。$push(要素追加)、$pull(条件一致要素を削除)、$addToSet(重複排除して追加)、$pop(先頭・末尾要素を削除)、また位置演算子$と$[]を使用して配列内の特定の要素を正確に更新できます。
仕組み: 配列修飾子はドキュメント全体ではなく配列要素自体に作用します。$pushと$addToSetの主な違いは、$pushは無条件に要素を追加し(重複の可能性あり)、$addToSetは要素が既に存在するかどうかを先にチェックすることです(重複排除)。位置演算子$はフィルター条件と共に使用し、「最初にマッチした配列要素」を特定します。$[]は「全配列要素」を操作します。$[identifier] + arrayFiltersは「条件に基づく一括更新」を行います。
設計哲学: MongoDBは関連データの小量を配列内に埋め込むことを推奨します(商品レビューやユーザータグのリストなど)。しかし大きすぎる配列(数百要素を超える)は検索と更新のパフォーマンスに影響します。大量の関連データには、別コレクションと参照を使用することを推奨します。
graph TB
A[配列更新修飾子] --> B[要素追加<br/>$push / $addToSet]
A --> C[要素削除<br/>$pull / $pop]
A --> D[一括操作<br/>$each / $slice]
A --> E[位置更新<br/>$ / $[] / $[filter]]
style A fill:#cce5ff
| 修飾子 | 機能 | 重複排除 | 例 | 使用頻度 |
|---|---|---|---|---|
$push |
要素追加 | ❌ | { $push: { tags: "new" } } |
⭐⭐⭐ |
$addToSet |
重複排除して追加 | ✅ | { $addToSet: { tags: "new" } } |
⭐⭐ |
$pull |
条件一致要素を削除 | — | { $pull: { tags: "old" } } |
⭐⭐ |
$pop |
先頭/末尾要素を削除 | — | { $pop: { tags: 1 } } |
⭐ |
$each |
一括追加($pushと併用) | — | { $push: { tags: { $each: [...] } } } |
⭐⭐ |
$slice |
配列長を制限 | — | { $push: { tags: { $each: [...], $slice: -5 } } } |
⭐⭐ |
$position |
挿入位置を指定 | — | { $push: { tags: { $each: [...], $position: 0 } } } |
⭐ |
(1) $push:配列に要素を追加
// === 単一要素追加 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $push: { tags: 'bestseller' } }
);
// === 複数要素追加($each)===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $push: { tags: { $each: ['5g', 'amoled', 'fast-charging'] } } }
);
// === 配列サイズを制限($slice + $position)===
db.products.updateOne(
{ sku: 'PHONE-001' },
{
$push: {
tags: {
$each: ['new1', 'new2', 'new3'],
$slice: -5, // 最後の5件のみ保持
$position: 0 // 先頭から挿入
}
}
}
);
(2) $pull 条件一致要素を削除
// === 指定値を削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pull: { tags: 'old-tag' } }
);
// === 条件を満たす全要素を削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pull: { tags: { $in: ['outdated1', 'outdated2'] } } }
);
(3) $addToSet:重複排除して配列に追加
// === 存在しない要素のみ追加 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $addToSet: { tags: 'new-tag' } }
);
// tagsに'new-tag'が含まれている場合は追加しない
// === 複数追加($each)===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $addToSet: { tags: { $each: ['tag1', 'tag2'] } } }
);
(4) $pop 先頭または末尾要素を削除
// === 末尾要素を削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pop: { tags: 1 } }
);
// === 先頭要素を削除 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pop: { tags: -1 } }
);
(5) 配列要素の位置指定更新
// === 位置インデックスで更新 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "tags.0": "updated-first-tag" } }
);
// === $位置演算子で更新(最初にマッチした要素)===
db.products.updateOne(
{ sku: 'PHONE-001', "reviews.userId": 'user_001' },
{ $set: { "reviews.$.helpful": 10 } }
);
// userId='user_001'のコメントを見つけ、helpfulフィールドを設定
// === 全配列要素を一括更新 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "reviews.$[].status": "approved" } }
);
// 全コメントのstatus → approved
(6) $[] 全要素を更新
// === 全配列要素を更新 ===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "reviews.$[].status": "approved" } }
);
// === 条件に基づいて配列要素を更新(arrayFilters)===
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "reviews.$[lowRating].flagged": true } },
{
arrayFilters: [{ "lowRating.rating": { $lt: 2 } }]
}
);
// rating < 2のコメントのみ対象
▶ サンプル 4:実践的な配列更新
// === シーン:eコマースレビューシステム ===
// 1. コメントを追加
db.products.updateOne(
{ sku: 'PHONE-001' },
{
$push: {
reviews: {
userId: 'user_001',
rating: 5,
content: 'Excellent phone!',
createdAt: new Date(),
helpful: 0
}
}
}
);
// 2. 特定ユーザーのコメントを削除
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $pull: { reviews: { userId: 'user_001' } } }
);
// 3. 低評価のレビューにフラグを立てる
db.products.updateOne(
{ sku: 'PHONE-001' },
{ $set: { "reviews.$[r].flagged": true } },
{ arrayFilters: [{ "r.rating": { $lt: 2 } }] }
);
// 4. コメント数を最大100件に制限
db.products.updateOne(
{ sku: 'PHONE-001' },
{
$push: {
reviews: {
$each: [newReview],
$slice: -100
}
}
}
);
8. upsertオプション
概念説明明: UPSERT = UPDATE + INSERTです。MongoDB特有の書き込みモードで、ドキュメントが存在すれば更新、存在しなければ挿入します。このモードは「べき等な書き込み」シーンで重要です。操作回数に関わらず結果が一貫性を保ちます。典型的なシーン:ユーザーログイン履歴(毎日初回ログイン時に作成、以降は更新)、ショッピングカート(初回アイテム追加時に作成、以降は数量更新)、設定値(初回設定時に作成、以降は値変更)。
仕組み: upsert: trueが設定されている場合、MongoDBはまずフィルターでドキュメントをマッチしようとします。マッチしたドキュメントが見つかれば、更新修飾子を適用します(通常のupdateOneと同様)。マッチしたドキュメントが見つからなければ、フィルター内の等価条件と更新修飾子の$set/$setOnInsertを組み合わせて新しいドキュメントを挿入します。$setOnInsertは挿入時のみ有効になり、更新時は無視されます。これがデフォルト値を設定する最良の方法です。
graph TB
A[updateOne + upsert: true] --> B{フィルターにマッチするドキュメントあり?}
B -->|はい| C[$set更新修飾子を適用]
B -->|いいえ| D[フィルター条件 + $set + $setOnInsertをマージ]
D --> E[新しいドキュメントを挿入]
C --> F[matchedCount=1を返却<br/>upsertedCount=0]
E --> G[matchedCount=0を返却<br/>upsertedCount=1<br/>upsertedId=ObjectId]
style C fill:#d4edda
style E fill:#fff3cd
| upsertの動作 | matchedCount | modifiedCount | upsertedCount | upsertedId |
|---|---|---|---|---|
| 見つかって更新 | 1 | 0または1 | 0 | null |
| 見つからず挿入 | 0 | 0 | 1 | ObjectId(...) |
| 見つからずupsertなし | 0 | 0 | 0 | null |
(1) upsertとは
upsert = update + insert; レコードが存在すれば更新、存在しなければ挿入。
graph TB
A[updateOne + upsert] --> B{ドキュメント存在?}
B -->|はい| C[$set更新を実行]
B -->|いいえ| D[新しいドキュメントを挿入<br/>$set + フィルターフィールドを適用]
style C fill:#d4edda
style D fill:#fff3cd
(2) Upsertの動作
// === upsert: false(デフォルト)===
const result1 = await Product.updateOne(
{ sku: 'NEW-001' },
{ $set: { price: 599 } }
);
print(result1.matchedCount); // 0(マッチなし)
print(result1.modifiedCount); // 0
print(result1.upsertedCount); // 0
// === upsert: true ===
const result2 = await Product.updateOne(
{ sku: 'NEW-001' },
{ $set: { price: 599 } },
{ upsert: true }
);
print(result2.upsertedCount); // 1(1件挿入)
print(result2.upsertedId); // ObjectId('...')
(3) $setOnInsert 挿入時のみ設定
// === 完全なupsertパターン ===
db.products.updateOne(
{ sku: 'NEW-001' },
{
$set: { price: 599, updatedAt: new Date() },
$setOnInsert: { createdAt: new Date(), stock: 0, viewCount: 0 }
},
{ upsert: true }
);
// === 存在しない場合に挿入:{ sku: 'NEW-001', price: 599, updatedAt: ..., createdAt: ..., stock: 0, viewCount: 0 }
// === 存在する場合に更新:{ sku: 'NEW-001', price: 599, updatedAt: ..., createdAt: <古い値> }
▶ サンプル 5:UPSERTの実践ガイド
// === ユーザーログイン履歴upsert ===
db.user_logins.updateOne(
{
userId: 'user_001',
date: '2026-07-01'
},
{
$set: { lastLoginAt: new Date() },
$inc: { loginCount: 1 },
$setOnInsert: { firstLoginAt: new Date() }
},
{ upsert: true }
);
// 毎日の初回ログイン時にレコードを作成、以降は更新
// === ショッピングカートupsert ===
db.carts.updateOne(
{ userId: 'user_001' },
{
$set: { updatedAt: new Date() },
$inc: { totalItems: 2 }
},
{ upsert: true }
);
9. 更新操作のベストプラクティス
概念説明明: 更新操作のベストプラクティスは3つの核心原則に集約されます:原子性(競合状態を回避)、パフォーマンス(ネットワーク往復とロック保持時間を削減)、安全性(誤操作を防止)。この中で原子性が最も重要です。MongoDBの単一ドキュメント操作は本質的に原子的ですが、「先に確認してから更新」パターンはこの原子性の保証を損ないます。
仕組み: MongoDBは単一ドキュメントの書き込み操作に対して原子性を保証します。updateOne操作は完全に成功するか完全に失敗するかのいずれかで、「更新が部分的に完了した」という中間状態はありません。ただし、複数ドキュメントにまたがる操作は自動的に原子性を提供しません(複数ドキュメントトランザクションは4.0以降で必要)。そのため、データモデルを設計する際は、関連データを同一ドキュメント内に配置し、単一ドキュメント原子性を活用するようにしてください。
graph TB
A[更新のベストプラクティス] --> B[原子性<br/>フィルター条件 + 原子修飾子]
A --> C[パフォーマンス<br/>bulkWrite + インデックス]
A --> D[安全性<br/>戻り値チェック + バージョン管理]
B --> B1[✅ 推奨:フィルター条件フィルタリング<br/>{ sku, stock: { $gt: 0 } }]
B --> B2[❌ 回避:先に確認してから更新<br/>findOne + updateOne]
style B1 fill:#d4edda
style B2 fill:#f8d7da
| 実践 | ベストプラクティス | アンチパターン |
|---|---|---|
| 同時実行安全性 | フィルター条件 + 原子操作 | findOne + updateOne |
| 一括更新 | bulkWrite + ordered: false | ループでupdateOne |
| バージョン管理 | $inc: { __v: 1 } | バージョン番号なし |
| エラー処理 | matchedCount/modifiedCountチェック | 戻り値を無視 |
(1) 原子性保証
// ✅ 安全:フィルター条件 + 原子操作
const result = await Product.updateOne(
{ sku: 'PHONE-001', stock: { $gt: 0 } },
{ $inc: { stock: -1 } }
);
// ❌ 安全でない:先に確認してから更新(競合状態)
const product = await Product.findOne({ sku: 'PHONE-001' });
if (product.stock > 0) {
await Product.updateOne(
{ sku: 'PHONE-001' },
{ $inc: { stock: -1 } }
);
}
(2) パフォーマンス最適化
概念概要: 更新操作のパフォーマンス最適化は3つの核心戦略に集約されます:ネットワーク往復の削減(bulkWriteを使用して繰り返しのupdateOne呼び出しを回避)、インデックスフィールドでのフィルタリング(全表スキャンを回避)、不要なドキュメント書き直しの回避(変更されたフィールドのみを更新)。この中でbulkWriteが最も大きなパフォーマンス向上をもたらします。100回の個別updateOne操作は約10秒かかりますが、1回のbulkWrite操作はわずか0.1秒です。
仕組み: updateOneを呼び出すたびに、ネットワーク上で完全な往復が発生します。クライアントがリクエストを送信 → サーバーがドキュメントをマッチ → 更新を適用 → 結果を返します。bulkWriteは100以上の操作を1回のネットワークリクエストにまとめます。サーバーは全操作を順次実行し、結果を一括で返します。また、WiredTigerストレージエンジンはドキュメント更新時にMVCCメカニズムを使用します。更新後にドキュメントサイズが増加し、元の場所に十分なスペースがない場合、ドキュメントは新しい場所に移動され、全インデックスエントリの更新がトリガーされます。そのため、ドキュメントサイズの変更を最小限に抑える(例:$incを使用して数値フィールド全体を書き直す)こともパフォーマンス上のメリットがあります。
| 最適化戦略 | パフォーマンス向上 | コード変更 | 推奨度 |
|---|---|---|---|
bulkWriteでupdateOneループを代替 |
10-100倍 | 中程度 | ⭐⭐⭐ |
| インデックスフィールドでのフィルタリング | 10-1,000倍 | 低い | ⭐⭐⭐ |
$incで数値フィールドの書き直しを代替 |
1.5-2倍 | 低い | ⭐⭐ |
| バッチサイズ制御(1,000件/バッチ) | 1.5-3倍 | 低い | ⭐⭐ |
| ドキュメントサイズの変更 | 1.2-1.5倍 | 低い | ⭐ |
// === 最適化1:一括更新で複数回の個別更新を代替 ===
// ❌ 低速:100回のupdateOne
for (const item of items) {
await Product.updateOne({ sku: item.sku }, { $inc: { stock: -item.qty } });
}
// ✅ 高速:1回のbulkWrite
await Product.bulkWrite(
items.map(item => ({
updateOne: {
filter: { sku: item.sku, stock: { $gte: item.qty } },
update: { $inc: { stock: -item.qty, soldCount: item.qty } }
}
})),
{ ordered: false }
);
// === 最適化2:インデックスフィールドでフィルタリング ===
// ✅ インデックスあり:db.products.updateOne({ sku: 'PHONE-001' }, ...)
// ⚠️ インデックスなし:db.products.updateOne({ title: 'Phone' }, ...)
(3) エラー処理
// === UpdateResult処理 ===
async function updateProductStock(sku, qty) {
const result = await Product.updateOne(
{ sku, stock: { $gte: qty } },
{ $inc: { stock: -qty, soldCount: qty } }
);
if (result.matchedCount === 0) {
throw new Error(`Out of stock or item not available: ${sku}`);
}
if (result.modifiedCount === 0) {
throw new Error('Update Failed');
}
return result;
}
❓ よくある質問
updateOneとupdateManyのパフォーマンス差はどの程度ですか?updateManyは1回の操作で複数ドキュメントを更新するため効率的ですが、より多くのドキュメントをロックします。一括更新にはbulkWriteとordered: falseの使用を推奨します。updateManyより柔軟です。$setはフィールドが存在しない場合エラーを投げますか?$setは自動的にフィールドを作成します(ネストフィールドを含む)。存在しないフィールドに対して$unsetを呼び出しても問題ありません。$incで浮動小数点数を使用した場合の精度損失はどう対処しますか?$incで浮動小数点数を使用すると精度の問題が発生する可能性があります。正確な計算にはDecimal128型(mongoose.Types.Decimal128)の使用を推奨します。$位置演算子(最初にマッチした要素を検索)または$[identifier] + arrayFilters(条件に基づいて複数要素を更新)を使用してください。replaceOneとupdateOneはいつ使い分けるべきですか?updateOne + $setを使用してください。ドキュメント全体を書き直す場合はreplaceOneを使用します。replaceOneは指定されていないフィールドを保持しないため、慎重に使用してください。📖 まとめ
- updateOneは単一ドキュメントを更新、updateManyは複数ドキュメントを更新
- replaceOne:ドキュメント全体を置き換え、未指定フィールドは削除される
- フィールド修飾子:$set/$unset/$inc/$mul/$rename/$min/$max/$currentDate/$setOnInsert
- 配列修飾子:$push/$pull/$addToSet/$pop/$each/$slice/$position
upsertオプション:存在しなければ挿入、$setOnInsertは挿入時のみ値を設定- 原子操作:フィルター条件 + 原子的更新で競合状態を回避
- bulkWrite:一括更新で最高パフォーマンス
📝 練習問題
- 基礎問題(⭐):
updateOneを使用して商品の価格、在庫、最終更新時刻を変更してください。 - 基礎問題(⭐):$pushを使用して商品に3つのタグを追加し、$addToSetで重複排除をテストしてください。
- 応用問題(⭐⭐):
bulkWriteを使用して注文の在庫引当を実装(複数アイテムの原子的引当)し、在庫不足のシナリオを処理してください。 - 応用問題(⭐⭐):
upsertを使用してユーザーの日次ログイン統計を実装(初回は作成、以降は増加)してください。 - チャレンジ(⭐⭐⭐):ショッピングカートマージ機能を実装し、仮カートのアイテムをユーザーカートに統合し、重複アイテムを処理(数量を集計)してください。