MongoDB: 更新文档:updateOne与updateMany详解

最后更新:2026-08-26

更新文档是 MongoDB 最常用的写操作之一——掌握 update 修饰符是数据修改的核心。

本课程深入学习 updateOne/updateMany/replaceOne、各种更新修饰符($set/$inc/$push/$pull)、upsert 行为和原子性保证。

1. 你将学到


2. 一个电商平台的真实故事

(1) 痛点:扣库存时遇到并发问题

Alice 在电商公司维护订单系统,扣库存时遇到了经典的并发问题:

JAVASCRIPT
// ❌ 反例:先查再改(竞态条件)
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 原子更新的解法

JAVASCRIPT
// ✅ 正例:用条件 + 原子操作
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 确认。整个操作对单文档是原子的——不会出现"只更新了一半字段"的中间状态。

100%
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) 基本语法

JAVASCRIPT
// === 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) 返回值解读

JAVASCRIPT
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) 没有匹配的处理

JAVASCRIPT
const result = await Product.updateOne(
  { sku: 'NOT_EXIST' },
  { $set: { stock: 0 } }
);
print(result.matchedCount);   // 0
print(result.modifiedCount);  // 0
// 不报错,仅不修改

▶ 示例 1:updateOne 实战

JAVASCRIPT
// === 修改单个字段 ===
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() } }
);

输出:

TEXT 📖 仅展示
// mongoose 操作成功执行
// 数据库查询/更新结果

4. updateMany 批量更新

概念说明updateMany 匹配所有符合条件的文档并统一应用更新操作。与 updateOne 只修改第一个匹配不同,updateMany 可以一次更新成千上万条文档。这是批量修改(如全场打折、批量下架、数据修复)的核心方法。

工作原理updateMany 先执行查询匹配所有符合条件的文档,再逐个应用更新操作。更新过程不是事务性的——如果中途失败,已更新的文档不会回滚。因此大批量更新需要考虑分批执行和错误处理。

维度 updateOne updateMany
匹配范围 第一个匹配 所有匹配
批量操作 ❌ 单条 ✅ 批量
事务回滚 ❌ 不支持 ❌ 不支持
适用场景 单条修改 批量打折、下架、修复
风险 较高(误操作影响大)

(1) 基本语法

JAVASCRIPT
// === updateMany 基本用法 ===
db.products.updateMany(
  { category: 'Electronics' },   // filter(多条匹配)
  { $set: { discount: 0.1 } }    // update(批量应用)
);

// 返回结果:
// {
//   acknowledged: true,
//   matchedCount: 250,     // 匹配 250 个
//   modifiedCount: 250,    // 修改 250 个
//   upsertedCount: 0
// }

(2) 批量更新注意事项

JAVASCRIPT
// ⚠️ 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:批量更新实战

JAVASCRIPT
// === 给所有 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' } }
);

输出:

TEXT 📖 仅展示
// 执行成功

5. replaceOne 替换整个文档

概念说明replaceOneupdateOne 的根本区别是——updateOne 使用修饰符部分更新字段,保留未指定的字段;replaceOne 完全替换文档内容,未指定的字段将被删除。这是 MongoDB 中最危险的操作之一,误用会导致数据丢失。

使用场景:仅当需要完全重写文档时使用 replaceOne(如数据迁移、文档格式升级)。大多数场景应使用 updateOne + $set,只修改需要的字段。

维度 updateOne + $set replaceOne
未指定字段 ✅ 保留 ❌ 删除
原子性 ✅ 单文档原子 ✅ 单文档原子
适用场景 部分字段修改 整文档重写
风险 高(字段丢失)

(1) 与 updateOne 的区别

JAVASCRIPT
// === 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 使用场景

JAVASCRIPT
// ✅ 适用:完全重写文档
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),但不能对同一字段使用多个修饰符。

100%
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 设置字段值

JAVASCRIPT
// === 设置字段 ===
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 删除字段

JAVASCRIPT
// === 删除单个字段 ===
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 增加值

JAVASCRIPT
// === 库存扣减 ===
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 乘法

JAVASCRIPT
// === 打 9 折 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $mul: { price: 0.9 } }
);

// === 价格翻倍 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $mul: { price: 2 } }
);

(5) $rename 重命名字段

JAVASCRIPT
// === 重命名字段 ===
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 取最小/最大值

JAVASCRIPT
// === $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 设置当前日期

JAVASCRIPT
// === 设置当前时间 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $currentDate: { lastModified: true } }
);

// === 设置为 Date 类型 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $currentDate: { lastModified: { $type: "date" } } }
);

(8) $setOnInsert upsert 时设置字段

JAVASCRIPT
// === 仅在 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:综合字段更新实战

JAVASCRIPT
// === 订单支付成功后更新订单状态 ===
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 }
  }
);

输出:

TEXT 📖 仅展示
// 执行成功

7. 数组更新修饰符

概念说明:数组是 MongoDB 文档中最灵活的数据结构,但更新数组元素比更新普通字段更复杂。MongoDB 提供了专门的数组修饰符——$push(添加元素)、$pull(删除匹配元素)、$addToSet(去重添加)、$pop(删除首尾元素),以及定位操作符 $$[] 用于精确更新数组中的特定元素。

工作原理:数组修饰符操作的是数组字段本身,而非整个文档。$push$addToSet 的关键区别是——$push 无条件添加(可能重复),$addToSet 先检查是否已存在(去重)。定位操作符 $ 配合 filter 条件定位"第一个匹配的数组元素",$[] 操作"所有数组元素",$[identifier] + arrayFilters 按"条件批量更新"。

设计哲学:MongoDB 鼓励将少量关联数据嵌入数组(如商品的评论列表、用户的标签),但数组过大(超过数百个元素)会影响查询和更新性能。对于大量关联数据,推荐使用独立集合 + 引用。

100%
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 添加数组元素

JAVASCRIPT
// === 添加单个元素 ===
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 删除匹配元素

JAVASCRIPT
// === 删除指定值 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pull: { tags: 'old-tag' } }
);

// === 删除满足条件的所有元素 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pull: { tags: { $in: ['outdated1', 'outdated2'] } } }
);

(3) $addToSet 数组去重添加

JAVASCRIPT
// === 仅添加不存在的元素 ===
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 删除首/尾元素

JAVASCRIPT
// === 删除最后一个元素 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pop: { tags: 1 } }
);

// === 删除第一个元素 ===
db.products.updateOne(
  { sku: 'PHONE-001' },
  { $pop: { tags: -1 } }
);

(5) 数组元素定位更新

JAVASCRIPT
// === 通过位置索引更新 ===
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) $[] 全部元素更新

JAVASCRIPT
// === 更新所有数组元素 ===
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:数组更新实战

JAVASCRIPT
// === 场景:电商评论系统 ===
// 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
      }
    }
  }
);

输出:

TEXT 📖 仅展示
// 执行成功

8. upsert 选项

概念说明:upsert = update + insert,是 MongoDB 独特的写入模式——文档存在则更新,不存在则插入。这个模式在"幂等写入"场景中至关重要:无论操作执行多少次,结果都是一致的。典型场景包括:用户登录记录(每天首次登录创建,后续更新)、购物车(首次添加创建,后续更新数量)、配置项(首次设置创建,后续修改值)。

工作原理:当 upsert: true 时,MongoDB 先尝试用 filter 匹配文档。如果找到匹配文档,应用更新修饰符(与普通 updateOne 相同)。如果未找到匹配文档,则将 filter 中的等值条件 + 更新修饰符中的 $set/$setOnInsert 合并为新文档插入。$setOnInsert 只在插入时生效,更新时忽略——这是设置默认值的最佳方式。

100%
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,存在则更新,不存在则插入。

100%
graph TB
    A[updateOne + upsert] --> B{文档存在?}
    B -->|是| C[执行 $set 等更新]
    B -->|否| D[插入新文档<br/>应用 $set + filter 中的字段]

    style C fill:#d4edda
    style D fill:#fff3cd

(2) upsert 行为

JAVASCRIPT
// === 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 仅插入时设置

JAVASCRIPT
// === 完整 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 实战

JAVASCRIPT
// === 用户登录记录 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 }
);

输出:

TEXT 📖 仅展示
// 执行成功

9. 更新操作的最佳实践

概念说明:更新操作的最佳实践围绕三个核心原则:原子性(避免竞态条件)、性能(减少网络往返和锁持有时间)、安全性(防止误操作)。其中原子性是最关键的——MongoDB 的单文档操作天然原子,但"先查后改"模式会破坏原子性保证。

工作原理:MongoDB 对单文档的写入操作提供原子性保证——一个 updateOne 操作要么完全成功,要么完全失败,不存在"只更新了一半"的中间状态。但跨文档的操作不自动提供原子性(需要 4.0+ 的多文档事务)。因此,设计数据模型时应尽量将相关数据放在同一文档中,利用单文档原子性。

100%
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) 原子性保证

JAVASCRIPT
// ✅ 安全: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
JAVASCRIPT
// === 优化 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) 错误处理

JAVASCRIPT
// === 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;
}

❓ 常见问题

Q updateOne 和 updateMany 的性能差多少?
A updateMany 在单次操作中更新多条,效率高,但会锁住更多文档。批量更新推荐用 bulkWrite + ordered: false,比 updateMany 更灵活。
Q $set 不存在的字段会报错吗?
A 不会。$set 会自动创建字段(包括嵌套字段)。$unset 不存在的字段也无害。
Q upsert 插入时的 _id 怎么生成?
A 自动使用 filter 中已声明的 _id 字段值;如果 filter 没有 _id,MongoDB 自动生成 ObjectId。
Q $inc 浮点数精度丢失怎么办?
A $inc 浮点数会有精度问题。推荐用 Decimal128 类型(mongoose.Types.Decimal128)做精确计算。
Q 数组字段更新怎么定位元素?
A$ 定位符(找到的第一个匹配元素)或 $[identifier] + arrayFilters(按条件批量更新)。
Q replaceOne 和 updateOne 的选择?
A 只想改几个字段用 updateOne + $set;整个文档重写用 replaceOne。replaceOne 不会保留未指定字段,慎用。

📖 小节


📝 作业

  1. 基础题(⭐):用 updateOne 修改商品价格、库存、最后更新时间。
  2. 基础题(⭐):用 $push 给商品添加 3 个标签,用 $addToSet 测试去重行为。
  3. 进阶题(⭐⭐):用 bulkWrite 实现订单扣库存(多个商品原子扣减),处理库存不足场景。
  4. 进阶题(⭐⭐):用 upsert 实现每日用户登录统计(首次创建,后续累加)。
  5. 挑战题(⭐⭐⭐):实现购物车合并功能,将临时购物车商品合并到用户购物车,处理重复商品(数量累加)。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏