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. 一个电商平台的真实故事
(1) 痛点:扣库存时遇到并发问题
Alice 在电商公司维护订单系统,扣库存时遇到了经典的并发问题:
// ❌ 反例:先查再改(竞态条件)
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) 收益
| 维度 | 先查后改 | 原子更新 |
|---|---|---|
| 并发安全 | ❌ 竞态条件 | ✅ 原子操作 |
| 性能 | ⚠️ 两次查询 | ⚡ 一次操作 |
| 代码复杂度 | 高 | 低 |
3. updateOne 更新单个文档
概念说明:updateOne 是 MongoDB 最常用的更新方法,根据过滤条件匹配第一个文档并应用更新操作。与 SQL 的 UPDATE ... SET ... WHERE ... 类似,但 MongoDB 使用更新修饰符(如 $set、$inc)指定修改内容,而非替换整个文档。这种设计使部分更新更加高效——只修改变化的字段,而非重写整个文档。
工作原理:updateOne 执行流程为:匹配阶段(根据 filter 找到文档)→ 更新阶段(应用更新修饰符)→ 索引更新(如果索引字段被修改)→ Write Concern 确认。整个操作对单文档是原子的——不会出现"只更新了一半字段"的中间状态。
sequenceDiagram
participant App as 应用程序
participant Mongo as MongoDB
participant WT as WiredTiger
App->>Mongo: updateOne({ sku: "PHONE-001" }, { $set: { price: 699 } })
Mongo->>Mongo: 匹配 filter(使用索引)
Mongo->>Mongo: 应用 $set 修改
Mongo->>Mongo: 检查索引是否需要更新
Mongo->>WT: 写入修改后的文档
WT-->>Mongo: 确认
Mongo-->>App: { matchedCount: 1, modifiedCount: 1 }
| 参数 | 类型 | 说明 |
|---|---|---|
filter |
Document | 匹配条件(必填) |
update |
Document | 更新操作(必填,须含修饰符) |
options |
Document | upsert / writeConcern 等(可选) |
| 返回字段 | 含义 | 注意事项 |
|---|---|---|
matchedCount |
匹配的文档数 | 可能为 0 |
modifiedCount |
实际修改的文档数 | 值相同但未变化时为 0 |
upsertedCount |
upsert 插入的文档数 | 仅 upsert: true 时可能为 1 |
(1) 基本语法
// === updateOne 基本用法 ===
db.products.updateOne(
{ sku: "PHONE-001" }, // filter
{ $set: { price: 699.99 } } // update
);
// 返回结果:
// {
// 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(没有插入)
| 字段 | 含义 |
|---|---|
matchedCount |
匹配 filter 的文档数 |
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
// 不报错,仅不修改
▶ 示例 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() } }
);
输出:
// mongoose 操作成功执行
// 数据库查询/更新结果
4. updateMany 批量更新
概念说明:updateMany 匹配所有符合条件的文档并统一应用更新操作。与 updateOne 只修改第一个匹配不同,updateMany 可以一次更新成千上万条文档。这是批量修改(如全场打折、批量下架、数据修复)的核心方法。
工作原理:updateMany 先执行查询匹配所有符合条件的文档,再逐个应用更新操作。更新过程不是事务性的——如果中途失败,已更新的文档不会回滚。因此大批量更新需要考虑分批执行和错误处理。
| 维度 | updateOne | updateMany |
|---|---|---|
| 匹配范围 | 第一个匹配 | 所有匹配 |
| 批量操作 | ❌ 单条 | ✅ 批量 |
| 事务回滚 | ❌ 不支持 | ❌ 不支持 |
| 适用场景 | 单条修改 | 批量打折、下架、修复 |
| 风险 | 低 | 较高(误操作影响大) |
(1) 基本语法
// === updateMany 基本用法 ===
db.products.updateMany(
{ category: 'Electronics' }, // filter(多条匹配)
{ $set: { discount: 0.1 } } // update(批量应用)
);
// 返回结果:
// {
// 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 商品打 9 折 ===
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()
}
);
// ❌ 不适用:只想修改一个字段(用 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 乘法
// === 打 9 折 ===
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 } }
);
// 如果当前 price > 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 先检查是否已存在(去重)。定位操作符 $ 配合 filter 条件定位"第一个匹配的数组元素",$[] 操作"所有数组元素",$[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 } }]
}
);
// 仅标记评分 < 2 的评论
▶ 示例 4:数组更新实战
// === 场景:电商评论系统 ===
// 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 先尝试用 filter 匹配文档。如果找到匹配文档,应用更新修饰符(与普通 updateOne 相同)。如果未找到匹配文档,则将 filter 中的等值条件 + 更新修饰符中的 $set/$setOnInsert 合并为新文档插入。$setOnInsert 只在插入时生效,更新时忽略——这是设置默认值的最佳方式。
graph TB
A[updateOne + upsert: true] --> B{filter 匹配文档?}
B -->|是| C[应用 $set 等更新修饰符]
B -->|否| D[合并 filter 等值 + $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 + filter 中的字段]
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. 更新操作的最佳实践
概念说明:更新操作的最佳实践围绕三个核心原则:原子性(避免竞态条件)、性能(减少网络往返和锁持有时间)、安全性(防止误操作)。其中原子性是最关键的——MongoDB 的单文档操作天然原子,但"先查后改"模式会破坏原子性保证。
工作原理:MongoDB 对单文档的写入操作提供原子性保证——一个 updateOne 操作要么完全成功,要么完全失败,不存在"只更新了一半"的中间状态。但跨文档的操作不自动提供原子性(需要 4.0+ 的多文档事务)。因此,设计数据模型时应尽量将相关数据放在同一文档中,利用单文档原子性。
graph TB
A[更新最佳实践] --> B[原子性<br/>filter + 原子修饰符]
A --> C[性能<br/>bulkWrite + 索引]
A --> D[安全性<br/>返回值检查 + 版本控制]
B --> B1[✅ 推荐: filter 条件过滤<br/>{ sku, stock: { $gt: 0 } }]
B --> B2[❌ 避免: 先查后改<br/>findOne + updateOne]
style B1 fill:#d4edda
style B2 fill:#f8d7da
| 实践 | 推荐做法 | 反模式 |
|---|---|---|
| 并发安全 | filter + 原子操作 | findOne + updateOne |
| 批量更新 | bulkWrite + ordered: false | 循环 updateOne |
| 版本控制 | $inc: { __v: 1 } | 无版本号 |
| 错误处理 | 检查 matchedCount/modifiedCount | 忽略返回值 |
(1) 原子性保证
// ✅ 安全:filter + 原子操作
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) 性能优化
概念说明:更新操作的性能优化围绕三个核心策略:减少网络往返(bulkWrite 替代循环 updateOne)、使用索引字段过滤(避免全表扫描)、避免不必要的文档重写(只修改变化的字段)。其中 bulkWrite 的性能提升最显著——100 次单独 updateOne 约需 10 秒,1 次 bulkWrite 仅需 0.1 秒。
工作原理:每次 updateOne 都是一次完整的网络往返——客户端发送请求 → 服务端匹配文档 → 应用更新 → 返回结果。bulkWrite 将 100+ 个操作合并为一次网络请求,服务端按序执行所有操作后一次性返回结果。此外,WiredTiger 存储引擎在文档更新时采用 MVCC 机制——如果更新后文档大小增加且原位置空间不足,文档会被移动到新位置,触发所有索引条目的更新。因此,减少文档大小变化(如 $inc 替代 $set 重写整个数值字段)也有性能收益。
| 优化策略 | 性能提升 | 代码改动 | 推荐度 |
|---|---|---|---|
bulkWrite 替代循环 updateOne |
10-100x | 中 | ⭐⭐⭐ |
| 索引字段过滤 | 10-1000x | 低 | ⭐⭐⭐ |
$inc 替代重写数值字段 |
1.5-2x | 低 | ⭐⭐ |
| 批量大小控制(1000/批) | 1.5-3x | 低 | ⭐⭐ |
| 减少文档大小变化 | 1.2-1.5x | 低 | ⭐ |
// === 优化 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(`库存不足或商品不存在: ${sku}`);
}
if (result.modifiedCount === 0) {
throw new Error('更新失败');
}
return result;
}
❓ 常见问题
$ 定位符(找到的第一个匹配元素)或 $[identifier] + arrayFilters(按条件批量更新)。📖 小节
- updateOne 更新单个文档,updateMany 批量更新
- replaceOne 替换整个文档,会丢失未指定字段
- 字段修饰符:$set/$unset/$inc/$mul/$rename/$min/$max/$currentDate/$setOnInsert
- 数组修饰符:$push/$pull/$addToSet/$pop/$each/$slice/$position
- upsert 选项:不存在则插入,$setOnInsert 仅插入时设置
- 原子操作:filter 条件 + 原子更新,避免竞态条件
- bulkWrite 批量更新性能最优
📝 作业
- 基础题(⭐):用 updateOne 修改商品价格、库存、最后更新时间。
- 基础题(⭐):用 $push 给商品添加 3 个标签,用 $addToSet 测试去重行为。
- 进阶题(⭐⭐):用 bulkWrite 实现订单扣库存(多个商品原子扣减),处理库存不足场景。
- 进阶题(⭐⭐):用 upsert 实现每日用户登录统计(首次创建,后续累加)。
- 挑战题(⭐⭐⭐):实现购物车合并功能,将临时购物车商品合并到用户购物车,处理重复商品(数量累加)。