4. CRUD操作与批量写入

MongoDB 的日常开发工作几乎都围绕 CRUD 展开:创建(Create)、读取(Read)、更新(Update)、删除(Delete)。与关系型数据库不同,MongoDB 以文档为最小操作单位,一次写入可以携带完整的嵌套结构,一次更新也可以只修改文档中的某个字段。理解每个方法的语义边界、返回值含义以及写关注(writeConcern)的作用,是写出可靠数据访问层的前提。

一、写入:insertOne 与 insertMany

insertOne 向集合插入单个文档,返回结果中包含 acknowledged 与 insertedId。若文档未显式提供 _id,驱动会在客户端生成一个 ObjectId 并写入。示例:

db.users.insertOne({
  name: "林晓",
  age: 28,
  tags: ["vip", "active"],
  createdAt: new Date()
})
// 返回:{ acknowledged: true, insertedId: ObjectId("...") }

insertMany 接收文档数组,默认按顺序逐条插入。它有一个关键参数 ordered:默认 true,遇到某条文档出错(如唯一索引冲突)会立即停止,后续文档不再插入;设为 false 时,MongoDB 会继续尝试剩余文档,最终在返回结果的 writeErrors 中汇总失败项。批量导入场景通常选择 ordered: false,以最大化写入吞吐并一次性收集所有冲突。

单次 insertMany 的文档总大小受 16MB 消息上限约束。大批量导入应分批提交,例如每批 1000 条,避免单次请求过大。

二、读取:find 与查询条件

find 返回游标(cursor),而非一次性返回全部结果。游标支持链式调用 sort、skip、limit、projection。查询条件使用查询操作符表达:$gt、$in、$and、$or、$regex 等。投影用于控制返回字段,{ name: 1, _id: 0 } 表示只返回 name 且排除 _id。

db.users.find(
  { age: { $gte: 18 }, tags: "vip" },
  { name: 1, age: 1, _id: 0 }
).sort({ age: -1 }).limit(20)

注意 tags: "vip" 这种写法对数组字段是“包含匹配”,即数组中任一元素等于 vip 即命中。若需要精确匹配整个数组,应使用 { tags: ["vip","active"] }。查询条件的选择直接影响能否命中索引,开发阶段可用 explain("executionStats") 观察 totalDocsExamined 与 nReturned 的比例。

三、更新:updateOne、updateMany 与 replaceOne

更新操作必须区分“修改字段”和“替换文档”两类语义。updateOne 与 updateMany 使用更新操作符,如 $set、$inc、$push、$unset,只改动指定字段,其余字段保留。若传入的更新文档不含任何操作符,MongoDB 会将其视为整体替换,这是常见误用。

// 正确:只改 age 字段
db.users.updateOne(
  { name: "林晓" },
  { $set: { age: 29 }, $push: { tags: "premium" } }
)

// 危险:整个文档被替换,name 之外的字段全部丢失
db.users.updateOne({ name: "林晓" }, { age: 29 })

replaceOne 的语义就是整体替换:用新文档完全取代匹配到的第一个文档,仅保留 _id。它适合“整条记录重写”的场景,例如从外部系统同步一条完整快照。返回值中 matchedCount 表示匹配数量,modifiedCount 表示实际发生变更的数量——若新值与旧值相同,modifiedCount 可能为 0,这并不代表操作失败。

三个更新方法都支持 upsert: true。当没有文档匹配时,MongoDB 会依据查询条件与更新内容自动构造并插入一条新文档。这在“存在则更新、不存在则创建”的配置类数据中非常实用,但要注意查询条件中的等值字段会被带入新文档。

四、删除:deleteOne 与 deleteMany

deleteOne 删除匹配到的第一条文档,deleteMany 删除所有匹配文档。两者都返回 deletedCount。删除是不可逆操作,生产环境应先用相同条件执行 find 确认影响范围,再执行删除。若集合数据量较大,deleteMany 可能长时间持有锁并产生大量 oplog,建议按条件分批删除。

五、bulkWrite:混合批量操作

当业务需要在一个批次中混合执行插入、更新、删除时,bulkWrite 比多次单独调用更高效,因为它减少了网络往返。它接收操作数组,每个元素形如 { insertOne: {...} }、{ updateOne: { filter, update, upsert } }、{ deleteMany: { filter } }。同样支持 ordered 参数控制遇错是否继续。

db.users.bulkWrite([
  { insertOne: { document: { name: "陈默", age: 31 } } },
  { updateOne: { filter: { name: "林晓" }, update: { $inc: { age: 1 } } } },
  { deleteMany: { filter: { tags: "expired" } } }
], { ordered: false })

返回结果按操作类型分组统计:insertedCount、matchedCount、modifiedCount、deletedCount,以及 upsertedCount。批量操作不会自动回滚,部分成功是正常状态,业务层需要根据返回结果决定是否补偿。

六、写关注:writeConcern 的取舍

写关注决定一次写操作在返回成功前需要被多少个节点确认。常用级别:w:1 表示主节点写入内存即返回,延迟最低但主节点故障时可能丢失;w:"majority" 表示多数节点确认,可靠性高但延迟增加;w:0 表示不等待确认,仅用于日志类可容忍丢失的数据。此外还有 j:true 要求写入 journal 日志,wtimeout 设置等待超时。

db.orders.insertOne(
  { orderId: "A1001", amount: 299 },
  { writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)

写关注可以在连接串、数据库、集合或单次操作级别设置,优先级由内向外覆盖。金融、订单类数据建议使用 majority;埋点、行为日志可使用 w:1 换取吞吐。需要强调的是,写关注只影响“确认级别”,不改变操作的原子性——单文档操作始终是原子的。

CRUD 操作语义与写关注决策图 操作类型 insertOne 单文档插入 insertMany ordered 控制遇错行为 find 返回游标,链式过滤 updateOne/Many $set/$inc 局部修改 replaceOne 整体替换,仅留 _id deleteOne/Many 返回 deletedCount bulkWrite 混合批量操作 insertOne updateOne deleteMany ordered:false 部分成功 写关注 writeConcern w: 0 不等待确认 · 可容忍丢失 日志、埋点 w: 1 主节点内存确认 · 低延迟 一般业务数据 w: "majority" 多数节点确认 · 高可靠 订单、金融数据 j: true + wtimeout 落盘日志 · 超时保护 关键写入路径 确认级别 返回值判读 matchedCount modifiedCount deletedCount

七、实践中的关键判断

在实际开发中,选择哪个方法往往取决于三个问题:操作对象是单条还是多条?是局部修改还是整体替换?对可靠性的要求有多高?例如用户资料更新用 updateOne + $set;配置同步用 replaceOne + upsert;订单创建用 insertOne + w:"majority";批量导入用 insertMany + ordered:false;混合批处理用 bulkWrite。

另一个容易忽略的点是返回值判读。很多开发者只检查操作是否抛出异常,却忽略了 matchedCount 为 0 的情况——这意味着条件没有命中任何文档,业务上可能需要走创建分支。同样,modifiedCount 为 0 也不代表失败,而是新值与旧值一致。把这些返回值纳入业务判断,才能避免“静默失败”。

更新文档时若忘记使用 $set 等操作符,MongoDB 会执行整体替换,导致未指定字段全部丢失。这是生产环境中最常见的数据事故之一,建议在代码审查中重点检查更新语句是否包含操作符。

最后,写关注与读关注(readConcern)配合使用才能构成完整的一致性策略。写关注保证数据被足够多节点确认,读关注保证读取到足够新的数据。对于“写后立即读”的场景,使用 w:"majority" 写入后,再以 readConcern:"majority" 读取,可以避免读到尚未同步的旧数据。理解这一对参数的配合关系,是构建可靠 MongoDB 应用的基础。