大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
CRUD 操作
这一课你将能做到
- 在 mongosh 中完成
insertOne/insertMany/find/updateOne/deleteOne全套操作,并说出与 SQL DML 的对应关系 - 正确使用
$set、$inc、$unset、$push、$currentDate更新文档,解释「为什么不能像改 JS 对象一样直接替换字段」 - 用
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"]
讲清楚要点
- 更新参数带运算符 = 补丁,不带 = 整篇替换:改字段用
$set,全文替换走replaceOne且要自己补全所有字段——两者意图必须分清。 updateOne只改第一条匹配:条件要能唯一定位(用_id最稳);批量改用updateMany且先用find预览匹配范围。$inc是服务端原子自增:并发计数不会丢更新,适合浏览量、做题次数;不要「先 find 再 +1 再 save」,那有竞态。- upsert = update + insert 幂等组合:配
$setOnInsert记录创建时间,是防重、防重复初始化的利器。 insertMany默认有序:中途失败则后续不执行;允许部分失败时用{ ordered: false }提高吞吐。- 删除前先 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 |
卡点急救
- 现象:更新后字段全没了
用了无运算符文档做 update,触发整篇替换。立刻
$set补回字段;长期靠团队规范 review 更新语句。 - 现象:
upsertedId每次都出现,永远在插入新文档 filter 里有每次都变的值(比如把new Date()放进条件)。固定 filter 字段,时间类字段挪到$set/$setOnInsert。 - 现象:
insertMany部分失败不知道哪些进了 返回结果的insertedIds是部分成功的;改用{ ordered: false }让其余继续,再按_id抽查补齐。
面试常问
-
MongoDB 的 updateOne 与 replaceOne 有什么区别?
updateOne配合$set等运算符做局部补丁,只动指定字段,其余保留;replaceOne用传入文档整篇替换(除_id外全部覆盖)。误把替换当补丁是常见线上事故来源。 -
说说 MongoDB 的原子性。 单文档操作天然原子:一次 update 内对同一文档的多个字段修改要么全生效要么全不生效,
$inc并发自增安全。跨文档需要 4.0+ 的多文档事务(要求副本集)。因此建模上常把「一起变的数据」内嵌进同一文档,用单文档原子性替代事务。 -
upsert 是什么?怎么实现幂等写入? update 时加
{ upsert: true }:匹配到就更新,匹配不到就按 filter + 更新内容插入。配合$setOnInsert只在插入时写创建时间,可安全重跑——常用于日志去重、初始化脚本、定时任务的幂等落库。
自测三问
updateOne与replaceOne的语义差别是什么?$setOnInsert解决什么问题?和$set同时用会怎样?- 为什么「先查再改再存」在并发下不安全,正确做法是什么?
小结一句话
insertOne / find / updateOne / deleteOne 是 CRUD 四件套:改字段用 $set 等运算符,计数用 $inc,幂等用 upsert,删除前必须收窄条件。
延伸
- MongoDB CRUD Operations
- Update Operators 更新运算符
- 下一课:查询与投影——把 find 的过滤与字段裁剪写到位。