CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
MongoDB 入门/课时/CRUD 操作
RawBlame
课时目录12 节

MongoDB 入门

  1. 01MongoDB 是什么
  2. 02安装与 mongosh
  3. 03文档与 BSON
  4. 04CRUD 操作
  5. 05查询与投影
  6. 06索引
  7. 07聚合管道
  8. 08Spring Data MongoDB
  9. 09何时用 Mongo vs SQL
  10. 10Docker 运行 MongoDB
  11. 11Node.js 驱动与 Mongoose
  12. 12综合练习

CRUD 操作

insertOne/find/updateOne/deleteOne。

35 分钟

CRUD 操作

这一课你将能做到

  1. 在 mongosh 中完成 insertOne / insertMany / find / updateOne / deleteOne 全套操作,并说出与 SQL DML 的对应关系
  2. 正确使用 $set、$inc、$unset、$push、$currentDate 更新文档,解释「为什么不能像改 JS 对象一样直接替换字段」
  3. 用 upsert 实现「不存在则插入」的幂等写入,并说清误用 deleteMany 的风险

为什么要学

所有业务最终都落在增删改查。MongoDB 的方法名很直观,但有一个新手必翻车的点:更新必须用更新运算符——直接传一个普通文档当更新参数,语义会变成「整篇替换」而不是「改某个字段」。

本课的例子全程围绕本站场景:做题日志 lesson_logs(谁在什么时候看了哪节课)。对齐官方 CRUD 文档;更复杂的过滤与投影留给下一课。

人话:insert 放进去,find 找出来,update 用运算符改字段,delete 删掉——语义与 SQL 同源,写法不同。

先建立图像

SQL                    MongoDB(集合 lesson_logs)
INSERT INTO …     ↔    insertOne / insertMany
SELECT … FROM …   ↔    find / findOne
UPDATE … SET …    ↔    updateOne / updateMany + $set 等运算符
DELETE FROM …     ↔    deleteOne / deleteMany

更新调用的三段结构,先在脑子里固化:

collection.updateOne(
  { 条件 },              // 第 1 段:filter,像 WHERE
  { $set: { … } },       // 第 2 段:update,必须是更新运算符
  { upsert: true }       // 第 3 段:options,可选
)

跟着做

步骤 1:准备集合并插入

use coderoadmap_lab

// 清空练习数据(仅限实验库!)
db.lesson_logs.deleteMany({})

// 单条插入
db.lesson_logs.insertOne({
  lessonSlug: "mongo-crud",
  userId: "u_1001",
  action: "view",
  viewCount: 0,
  at: new Date()
})

// 批量插入(ordered: false 时某条失败不影响后续)
db.lesson_logs.insertMany([
  { lessonSlug: "mongo-crud",   userId: "u_1002", action: "view",     viewCount: 0, at: new Date() },
  { lessonSlug: "mongo-intro",  userId: "u_1001", action: "complete", viewCount: 1, at: new Date() }
])

db.lesson_logs.countDocuments()   // 应为 3

步骤 2:查询

// 查全部(仅练习库数据少时这么干;生产查询必须带条件)
db.lesson_logs.find()

// 带条件 + 只取一条
db.lesson_logs.findOne({ userId: "u_1001", action: "complete" })

应看到:find 返回游标,mongosh 自动打印前 20 条;findOne 直接返回文档或 null。

步骤 3:更新——必须用运算符

const id = db.lesson_logs.findOne(
  { userId: "u_1001", lessonSlug: "mongo-crud" }
)._id

db.lesson_logs.updateOne(
  { _id: id },
  {
    $set: { action: "complete" },      // 设置 / 新增字段
    $inc: { viewCount: 1 },            // 数值原子自增(字段不存在从 0 开始)
    $currentDate: { updatedAt: true }  // 写入服务器当前时间
  }
)

验证:

db.lesson_logs.findOne({ _id: id })
// action="complete"、viewCount=1、updatedAt 出现

再试一次错误姿势,体会语义差异:

// 传入无运算符的文档 = 整篇替换(只保留 _id + action),其他字段全丢!
db.lesson_logs.replaceOne({ _id: id }, { action: "demo" })
db.lesson_logs.findOne({ _id: id })   // userId、viewCount 都没了

按下面的 $set 恢复字段,继续后面的步骤。

步骤 4:upsert——不存在则插入

db.lesson_logs.updateOne(
  { userId: "u_9999", lessonSlug: "mongo-crud" },
  {
    $set: { action: "view", viewCount: 1 },
    $setOnInsert: { at: new Date() },   // 只在真正插入时生效
    $currentDate: { updatedAt: true }
  },
  { upsert: true }
)

应看到:第一次返回 upsertedId(新插入),再跑一次返回 modifiedCount: 1(只更新)。这正是「记录用户首次做题时间」这类幂等写入的标准写法。

步骤 5:删除——先数再删

// 危险操作前的固定动作:先确认影响范围
db.lesson_logs.countDocuments({ userId: "u_9999" })   // 1
db.lesson_logs.deleteOne({ userId: "u_9999" })

// deleteMany({}) 会清空整个集合,仅练习库可用

步骤 6:数组字段操作(预告建模威力)

db.users.insertOne({ userId: "u_1001", name: "同学A", badges: ["streak-7"] })

db.users.updateOne(
  { userId: "u_1001" },
  { $addToSet: { badges: "mongo-fan" } }   // 去重追加;$push 是不去重追加
)
db.users.findOne({ userId: "u_1001" })    // badges: ["streak-7", "mongo-fan"]

讲清楚要点

  1. 更新参数带运算符 = 补丁,不带 = 整篇替换:改字段用 $set,全文替换走 replaceOne 且要自己补全所有字段——两者意图必须分清。
  2. updateOne 只改第一条匹配:条件要能唯一定位(用 _id 最稳);批量改用 updateMany 且先用 find 预览匹配范围。
  3. $inc 是服务端原子自增:并发计数不会丢更新,适合浏览量、做题次数;不要「先 find 再 +1 再 save」,那有竞态。
  4. upsert = update + insert 幂等组合:配 $setOnInsert 记录创建时间,是防重、防重复初始化的利器。
  5. insertMany 默认有序:中途失败则后续不执行;允许部分失败时用 { ordered: false } 提高吞吐。
  6. 删除前先 count:生产环境禁止无条件的 deleteMany({});大删除要分批 + 备份。

你可能会懵的地方

  • 为什么不能 updateOne(条件, { action: "complete" })? 这会被解释为「用这个文档替换整篇」。MongoDB 把「补丁更新」与「全文替换」分成两种语义,后者有专门的 replaceOne。改字段永远走 $set。

  • find 和 findOne 差在哪? find 返回游标可迭代多篇;findOne 返回单篇或 null(内部就是 find + limit 1)。明确只要一条时用 findOne,代码意图更清晰。

  • modifiedCount 为 0 是失败吗? 不一定。看 matchedCount:为 0 说明条件没匹配;匹配了但新值与旧值相同,modifiedCount 也是 0(无实际变更)。

  • 并发同时 $inc 会丢更新吗? 不会。单文档操作原子执行,$inc 在服务端完成,天然并发安全——这正是文档原子性的价值。

  • 单文档原子,那跨文档呢? 需要多文档事务(4.0+,要求副本集环境)。本站强一致账本仍在 MySQL;Mongo 里尽量用「把一起变的数据放一个文档」来规避跨文档事务。

错法对照

错法问题正解
updateOne(f, { field: v }) 当补丁用整篇替换,字段丢失用 $set;全文替换才用 replaceOne
updateMany 条件写得过宽误改大量文档先 find 确认匹配集,再按 _id 或精确条件改
deleteMany({}) 当「清一下」全集合数据消失带精确过滤;删除前 count;只在实验库这么干
先 find 取值 → 应用层计算 → save 回写并发下丢失更新$inc / $set 等服务端原子运算符
把 insertOne 返回值当文档用它只是写入结果元数据之后用 _id 再 findOne

卡点急救

  1. 现象:更新后字段全没了 用了无运算符文档做 update,触发整篇替换。立刻 $set 补回字段;长期靠团队规范 review 更新语句。
  2. 现象:upsertedId 每次都出现,永远在插入新文档 filter 里有每次都变的值(比如把 new Date() 放进条件)。固定 filter 字段,时间类字段挪到 $set / $setOnInsert。
  3. 现象:insertMany 部分失败不知道哪些进了 返回结果的 insertedIds 是部分成功的;改用 { ordered: false } 让其余继续,再按 _id 抽查补齐。

面试常问

  • MongoDB 的 updateOne 与 replaceOne 有什么区别? updateOne 配合 $set 等运算符做局部补丁,只动指定字段,其余保留;replaceOne 用传入文档整篇替换(除 _id 外全部覆盖)。误把替换当补丁是常见线上事故来源。

  • 说说 MongoDB 的原子性。 单文档操作天然原子:一次 update 内对同一文档的多个字段修改要么全生效要么全不生效,$inc 并发自增安全。跨文档需要 4.0+ 的多文档事务(要求副本集)。因此建模上常把「一起变的数据」内嵌进同一文档,用单文档原子性替代事务。

  • upsert 是什么?怎么实现幂等写入? update 时加 { upsert: true }:匹配到就更新,匹配不到就按 filter + 更新内容插入。配合 $setOnInsert 只在插入时写创建时间,可安全重跑——常用于日志去重、初始化脚本、定时任务的幂等落库。

自测三问

  1. updateOne 与 replaceOne 的语义差别是什么?
  2. $setOnInsert 解决什么问题?和 $set 同时用会怎样?
  3. 为什么「先查再改再存」在并发下不安全,正确做法是什么?

小结一句话

insertOne / find / updateOne / deleteOne 是 CRUD 四件套:改字段用 $set 等运算符,计数用 $inc,幂等用 upsert,删除前必须收窄条件。

延伸

  • MongoDB CRUD Operations
  • Update Operators 更新运算符
  • 下一课:查询与投影——把 find 的过滤与字段裁剪写到位。
加载本节练习…
← 上一课文档与 BSON下一课 →查询与投影