MongoDBNotes

第 04 章:CRUD

zjc 于 2026-01-04 发布

这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 CRUD 是 MongoDB 最常用的操作。核心原则是:写操作明确 _id 和更新策略,读操作明确投影、排序和分页,批量操作控制并发,所有高频查询都有索引支撑。

4.1 插入文档

插入单条:

db.orders.insertOne({
  _id: "o_202608250001",
  user_id: "u_1001",
  status: "CREATED",
  amount: NumberDecimal("199.00"),
  items: [
    { sku_id: "sku_10001", quantity: 1, price: NumberDecimal("199.00") }
  ],
  created_at: new Date()
})

插入多条:

db.products.insertMany([
  { sku_id: "sku_10001", title: "键盘", price: NumberDecimal("399.00") },
  { sku_id: "sku_10002", title: "鼠标", price: NumberDecimal("199.00") }
], { ordered: false })

ordered: false 允许继续处理后续文档,但应用仍需处理重复键等错误。

4.2 查询文档

查询单条:

db.orders.findOne({ _id: "o_202608250001" })

查询多条:

db.orders.find({
  user_id: "u_1001",
  status: { $in: ["CREATED", "PAID"] }
})

指定字段:

db.orders.find(
  { user_id: "u_1001" },
  { order_no: 1, status: 1, amount: 1, created_at: 1, _id: 0 }
)

排序和限制:

db.orders.find({ user_id: "u_1001" })
  .sort({ created_at: -1 })
  .skip(0)
  .limit(20)

大分页不建议使用大 skip,应使用范围条件游标分页。

4.3 更新文档

更新单条:

db.orders.updateOne(
  { _id: "o_202608250001", status: "CREATED" },
  {
    $set: { status: "PAID", paid_at: new Date() },
    $currentDate: { updated_at: true }
  }
)

更新多条:

db.products.updateMany(
  { status: "ON_SALE" },
  { $set: { channel: "online" } }
)

常用更新操作符:

操作符 说明
$set 设置字段
$unset 删除字段
$inc 数值递增
$mul 数值相乘
$push 数组添加
$pull 数组删除
$addToSet 去重添加
$currentDate 更新时间

不要用不带更新操作符的文档整体替换,除非确实要覆盖。

4.4 数组更新

添加标签:

db.products.updateOne(
  { sku_id: "sku_10001" },
  { $addToSet: { tags: "hot" } }
)

移除标签:

db.products.updateOne(
  { sku_id: "sku_10001" },
  { $pull: { tags: "cold" } }
)

更新数组中匹配元素:

db.orders.updateOne(
  {
    _id: "o_202608250001",
    "items.sku_id": "sku_10001"
  },
  {
    $set: { "items.$.quantity": 2 }
  }
)

数组位置更新要确认匹配条件唯一,否则可能更新到非预期元素。

4.5 删除文档

删除单条:

db.orders.deleteOne({ _id: "o_202608250001" })

删除多条:

db.orders.deleteMany({
  status: "CLOSED",
  created_at: { $lt: new Date("2026-01-01T00:00:00Z") }
})

删除建议:

  1. 生产删除前先查询确认数量;
  2. 保留审计数据时使用软删除;
  3. 大量删除分批执行;
  4. 删除条件必须有索引;
  5. 删除不可轻易回滚。

4.6 批量写入

db.orders.bulkWrite([
  { insertOne: { document: { _id: "o_1", status: "CREATED" } } },
  { updateOne: {
      filter: { _id: "o_1", status: "CREATED" },
      update: { $set: { status: "PAID" } }
  }},
  { deleteOne: { filter: { _id: "o_2", status: "CANCELLED" } } }
], { ordered: false })

批量写入可以减少网络往返,但要控制批次大小,并处理每类错误。

4.7 查找并修改

const result = db.orders.findOneAndUpdate(
  { _id: "o_202608250001", status: "CREATED" },
  { $set: { status: "PROCESSING" } },
  {
    returnDocument: "after",
    projection: { status: 1 },
    sort: { created_at: 1 }
  }
)

适用:

  1. 原子领取任务;
  2. 状态机流转;
  3. 计数器更新;
  4. 避免读改写竞争。

类似命令还有 findOneAndReplacefindOneAndDelete

4.8 乐观并发控制

文档版本字段:

db.orders.updateOne(
  { _id: "o_202608250001", version: 3 },
  {
    $set: { status: "PAID" },
    $inc: { version: 1 }
  }
)

matchedCount 为 0,说明版本变化或状态不满足条件,应重新读取业务对象。

4.9 写关注

插入:

db.orders.insertOne(
  { _id: "o_202608250003", status: "CREATED" },
  { writeConcern: { w: "majority", j: true, wtimeout: 3000 } }
)
参数 说明
w 等待多少节点确认
j 是否等待 journal
wtimeout 等待超时

重要交易数据通常使用 majority 写关注;日志类数据可以根据成本选择较低确认级别。

4.10 常见错误

问题 原因
Duplicate key _id 或唯一索引重复
Document validation failure validator 拒绝
Query exceeded memory 聚合或排序无索引
Update matched 0 条件不匹配或版本变化
Modified 0 值已相同
WriteConcernTimeout 副本确认超时
Not primary 写入 non-primary

本章小结

MongoDB CRUD 的关键是把“条件更新”作为默认思维方式,用状态机条件、版本号和原子更新避免读改写竞争。读操作应始终控制投影、排序和数量;写操作要明确写关注、批量边界和错误处理。

思考题

  1. updateOne 和整体替换有什么区别?
  2. 为什么大分页不推荐大 skip
  3. findOneAndUpdate 适合什么场景?
  4. 版本号如何实现乐观锁?
  5. majority 写关注解决什么问题?