4.3 CRUD 操作实战


4.3 CRUD 操作实战

数据模型(4.2)就位,本节把四类操作——增(Create)、读(Read)、改(Update)、删(Delete)——在 mongosh 里完整过一遍手。示例沿用 4.2 的商品文档,前后连贯;本节的查询与投影语法也是 4.4 索引优化和 4.7 聚合的地基。

增与删:一句插入,两种删除

插入有单文档与批量两种形态;删除则要分清"带条件的删"与"清空集合"的量级差异:

// 增:单条插入 db.products.insertOne({ sku: "SKU-90001", name: "速干T恤", stock: 50 }) // 增:批量插入(一次网络往返,写批量的推荐姿势) db.products.insertMany([ { sku: "SKU-90002", name: "防晒衣", stock: 30 }, { sku: "SKU-90003", name: "登山杖", stock: 80 } ]) // 删:按条件删除(先确认条件精确,删错不可恢复) db.products.deleteMany({ stock: 0 }) // 删:清空集合(保留集合定义),等效但不等于 drop 的元数据处理 db.products.deleteMany({})

插入时有一个高频追问:批量插入一半失败怎么办?默认有序插入(遇到错误即停,前面的保留),传入 ordered: false 则继续执行后续文档,最后汇总报错——批量导入脏数据的容错场景常用后者。

读:条件、投影与排序

读取的语法核心是"查询文档 + 投影文档 + 游标修饰"三件套:

// 条件:比较操作符 $gte $lte,逻辑操作符 $and $or $in db.products.find({ stock: { $gte: 10 }, $or: [ { name: /冲锋/ }, { tags: "秋季" } ] }) // 投影:1 返回 0 排除(_id 例外,默认返回) db.products.find( { sku: "SKU-88312" }, { name: 1, "attrs.size": 1, _id: 0 } ) // 排序 + 跳过 + 限量:列表页标配 db.products.find({ stock: { $gt: 0 } }) .sort({ updated: -1 }) .skip(20).limit(10)

三个工程细节。正则查询 /冲锋/ 前缀不带锚点时无法利用索引(4.4 会回到这点),前缀匹配 /^冲锋/ 可以。深分页陷阱:skip 翻到深处时,数据库要扫描并丢弃前 N 条,页码越大越慢;大偏移场景改用游标定位(记住上一页末尾的排序值,下一页查"大于该值"),复杂度恒定。计数的代价:countDocuments 精确计数在超集合上同样不便宜,能用估算(estimatedDocumentCount)或另行冗余计数(3.5 的摘要手法)就别硬算。

改:更新操作符与 upsert

更新操作符是文档模型的表达力所在——它让"修改"发生在服务端文档内部,而不是把整个文档读出来改完再写回:

// $set 修改字段 / $inc 原子自增 / $push 数组追加 db.products.updateOne( { sku: "SKU-88312" }, { $set: { name: "轻量冲锋衣 2 代" }, $inc: { stock: -1 }, $push: { reviews: { user: 1001, score: 5 } } } ) // upsert:查无则插入(findAndModify 语义,常用于计数器与去重) db.metrics.updateOne( { date: "2026-09-01", sku: "SKU-88312" }, { $inc: { pv: 1 } }, { upsert: true } )

$inc$push 的价值在原子性:并发下两个客户端同时给库存减一,服务端的更新操作符保证结果正确(读改写三步自己做则会互相覆盖)。更新还有一个必须记住的规则:整个更新操作在单文档内原子执行——把需要一起变的字段放进同一文档,很多"事务需求"就消失了。

演练:库存扣减的并发正确性全过程

背景:秒杀场景同一商品瞬时几百个并发下单,库存字段从 500 扣到 0,不允许超卖。

操作:朴素做法是"读 stock → 判断大于 0 → 写回 stock-1",并发下两个请求同时读到 1、都判断通过、都写 0,库存超卖。改用条件更新,把判断下沉到数据库端:

// 条件更新:库存充足才扣减,返回 matchedCount 判断成败 const result = db.products.updateOne( { sku: "SKU-88312", stock: { $gt: 0 } }, { $inc: { stock: -1 } } ) if (result.matchedCount === 1) { // 扣减成功,继续创建订单 }

结果:并发压测下零超卖;失败请求 matchedCount 为 0,返回"已售罄"。

解读:正确性来自把"判断 + 修改"合并成一个原子的条件更新——这是文档模型里最常用的并发控制手法,本质是乐观锁思想(3.4 提到的版本号条件写入是它的变体)。没有锁竞争、没有事务开销,一行查询解决。

变式:若业务规则复杂到条件写不下(扣库存还要校验限购数、活动资格),才升级到多文档事务(4.8)或引入 TCC 流程(3.4)。升级前先想想单文档原子性够不够,十次里九次够。

易错点

第一个是读改写反模式:把文档读进应用层、改完整篇写回,并发丢更新且流量翻倍;能用更新操作符就别读改写。第二个是删除不带索引条件:deleteMany 的条件列若没有索引,删除在全表扫描上执行,业务高峰期直接拖垮库;大批量删除按索引列分段进行。第三个是投影错觉:投影只是裁剪返回结果,不减少服务端读取量——想降低读取成本要靠索引覆盖(4.4),别指望投影。

本节要点回顾

  • 插入分单条与批量,ordered: false 是脏数据导入的容错开关。
  • 查询三件套:条件文档 + 投影 + 游标修饰;正则带前缀锚点才走索引。
  • 深分页用游标定位替代 skip,复杂度与页深无关。
  • 更新操作符set/inc/$push)在服务端原子生效,是并发正确性的第一道防线。
  • 秒杀式并发用条件更新(stock 大于 0 才扣)实现无锁乐观控制。

操作会用了,查询一慢怎么办——下一节索引与查询优化接住这个问题。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U