自在学

我们与你共同进步

  • 分类课程
  • 文章
  • 工作台
  • 订阅

  • 关于我们
  • 隐私政策
  • 使用条款

探索

  • 分类课程
  • 文章
  • 工作台
  • 订阅

网站信息

  • 关于我们
  • 隐私政策
  • 使用条款

加入社区

自在学学习社区微信二维码

微信扫码,交流学习

株洲市自在学教育科技有限公司© 2025 - 2026 版权所有

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

湘公网安备43020302000292号|湘ICP备2025148919号-1
分类课程工作台文章订阅
分类课程工作台文章价格

MongoDB 纸舟书店完整课程

  1. 01MongoDB 与纸舟书店
  2. 02启动 MongoDB 8.0
  3. 03BSON、集合与纸舟书店种子数据
  4. 04用 CRUD 推进纸舟书店业务
  5. 05查询、投影、排序与稳定分页
  6. 06原子更新、批量写入与并发语义
  7. 07用访问模式设计纸舟书店的数据
  8. 08用索引和 explain 量化查询成本
  9. 09用聚合管道回答纸舟书店的业务问题
  10. 10用 Node.js 22 连接纸舟书店
  11. 11分片、分片键与水平扩展
  12. 12纸舟书店的完整交付
  13. 13监控、慢查询与性能诊断
  14. 14副本集、故障转移与一致性
  15. 15从单文档原子性到事务下单
  16. 16备份、恢复与恢复验收
  17. 17认证、授权与安全边界
正在加载课程章节内容
课程编程MongoDB 纸舟书店完整课程用 CRUD 推进纸舟书店业务

用 CRUD 推进纸舟书店业务

CRUD 是 Create、Read、Update、Delete 的缩写,对应创建、读取、更新和删除。名字很基础,真正重要的是每个操作的三个问题:过滤条件会命中谁、修改内容是什么、返回结果能证明什么。

这一节继续使用上一节的种子状态。开始前应有 5 本书、3 位顾客和 5 张订单。如果状态不同,先重新执行上一节的集合创建与种子导入脚本。

纸舟书店创建、读取、按业务条件更新、安全删除和结果确认的 CRUD 项目流程手绘图


先确认起点

知识点

数据库操作具有状态。上一条命令是否执行过,会改变下一条命令的结果。与其在结果不一致时猜原因,不如先建立一个小而明确的前置检查。

实操

如果已经退出 mongosh,重新进入并切换数据库:

shell
docker exec -it paperboat-mongo mongosh --quiet
javascript
use bookstore
 
print(EJSON.stringify({
  books: db.books.countDocuments({}),
  customers: db.customers.countDocuments({}),
  orders: db.orders.countDocuments({})
}));

结果展示

json
{"books":5,"customers":3,"orders":5}

三个数字说明我们站在同一个起点。若结果不同,优先重置种子数据,而不是为了迁就错误状态去改下面的过滤条件。


创建一位新顾客

知识点

insertOne() 接收一个文档。集合存在时,它把文档写入集合;如果没有 _id,mongosh 会生成 ObjectId。课程仍使用固定字符串 _id,这样新增顾客可以被后续命令准确找到。

插入结果中的 acknowledged 说明服务端是否确认写入,insertedId 则告诉调用方最终使用了哪个 _id。它不会把整个新文档原样返回。

实操

javascript
const insertResult = db.customers.insertOne({
  _id: "customer-chen",
  name: "陈墨",
  email: "chen@example.com",
  addresses: [
    {
      label: "默认",
      city: "苏州",
      detail: "姑苏区墨香路 6 号"
    }
  ]
});
 
print(EJSON.stringify({
  acknowledged: insertResult.acknowledged,
  insertedId: insertResult.insertedId
}));

结果展示

json
{"acknowledged":true,"insertedId":"customer-chen"}

这两个字段证明写入被确认,并且新顾客的标识是 customer-chen。再读取一次文档,才能验证字段内容:

知识点

写入确认回答“数据库接受了吗”,读取回答“保存后的事实是什么”。对关键流程做读后验证,比只看到命令没有抛错更可靠。

实操

javascript
const newCustomer = db.customers.findOne(
  { _id: "customer-chen" },
  { _id: 1, name: 1, email: 1, "addresses.city": 1 }
);
 
print(EJSON.stringify(newCustomer));

结果展示

json
{"_id":"customer-chen","name":"陈墨","email":"chen@example.com","addresses":[{"city":"苏州"}]}

投影只返回了需要核对的字段。addresses 仍然是数组,但每个元素只保留 city。

固定 _id 让课程结果稳定,也会让重复执行 insertOne() 触发重复键错误。若你已经成功插入 customer-chen,不要再次执行创建命令;需要从头重做时,回到种子初始化步骤统一重置。


读取已发布书籍

知识点

find() 返回游标,而不是立即把无限结果全部放进内存。过滤器 { published: true } 决定哪些文档进入结果;第二个参数是投影,决定返回哪些字段。这里显式排除 _id,让输出只关注标题、价格和库存。

sort({ title: 1 }) 中的 1 表示升序,-1 表示降序。即使数据量很小,也应明确结果顺序,否则数据库没有义务每次按同一顺序返回。

实操

javascript
const publishedBooks = db.books
  .find(
    { published: true },
    { _id: 0, title: 1, price: 1, stock: 1 }
  )
  .sort({ title: 1 })
  .toArray();
 
print(EJSON.stringify(publishedBooks));

结果展示

json
[{"title":"MongoDB 从入门到实践","price":89,"stock":20},{"title":"Node.js 项目开发","price":69,"stock":15},{"title":"数据建模的艺术","price":79,"stock":8},{"title":"现代 Web 基础","price":59,"stock":30}]

结果只有 4 本,因为 book-draft 的 published 为 false。toArray() 适合当前这组很小的课程数据;面对大量文档时,应让应用逐批消费游标,不能无条件把全部结果载入内存。


精确读取一个文档

知识点

findOne() 返回第一个匹配文档;没有匹配时返回 null。当过滤条件使用唯一 _id 时,结果最多只有一条,语义比 find(...).limit(1) 更直接。

嵌套字段可以用点号路径查询。addresses.city 会检查地址数组中嵌套文档的 city 字段,只要至少一个元素匹配就能命中顾客。

实操

javascript
const hangzhouCustomer = db.customers.findOne(
  { "addresses.city": "杭州" },
  { _id: 1, name: 1, addresses: 1 }
);
 
print(EJSON.stringify(hangzhouCustomer));

结果展示

json
{"_id":"customer-zhou","name":"周雨","addresses":[{"label":"默认","city":"杭州","detail":"西湖区云栖街 12 号"}]}

点号路径用于过滤,并不会自动裁剪数组内容,所以投影中的 addresses: 1 返回完整地址数组。若顾客有多个地址且只想返回匹配元素,后面可以使用 $elemMatch 投影或聚合管道。


用条件更新完成一次售卖

知识点

售出一本书需要减少库存。我们没有先读取库存再决定更新,而是把“库存至少为 1”写进更新过滤器:

javascript
{ _id: "book-mongodb", stock: { $gte: 1 } }

MongoDB 会在执行写入时检查这个条件。$inc 对库存做增量修改,$set 把书标记为精选。两个操作符作用于同一个文档,因此这次更新是原子的,其他客户端不会看到库存已减但 featured 还没设置的中间状态。

实操

javascript
const updateResult = db.books.updateOne(
  {
    _id: "book-mongodb",
    stock: { $gte: 1 }
  },
  {
    $inc: { stock: -1 },
    $set: { featured: true }
  }
);
 
print(EJSON.stringify({
  acknowledged: updateResult.acknowledged,
  matchedCount: updateResult.matchedCount,
  modifiedCount: updateResult.modifiedCount
}));

结果展示

json
{"acknowledged":true,"matchedCount":1,"modifiedCount":1}

matchedCount: 1 表示过滤器找到一本库存充足的目标书;modifiedCount: 1 表示文档内容确实发生变化。现在读取更新后的字段:

知识点

更新结果只给计数,不返回修改后的完整文档。读后验证能区分“命中了文档”与“字段最终是什么”。

实操

javascript
const updatedBook = db.books.findOne(
  { _id: "book-mongodb" },
  { _id: 0, title: 1, stock: 1, featured: 1 }
);
 
print(EJSON.stringify(updatedBook));

结果展示

json
{"title":"MongoDB 从入门到实践","stock":19,"featured":true}

库存从 20 变成 19,featured 为 true。这两个值会成为下一节组合查询的输入状态。


理解 matchedCount 与 modifiedCount

知识点

命中文档不代表一定发生修改。如果把已经是 true 的 featured 再次设置为 true,过滤器仍然命中,但最终值没有变化。应用不能把 modifiedCount: 0 一律解释成“记录不存在”。

实操

javascript
const noChangeResult = db.books.updateOne(
  { _id: "book-mongodb" },
  { $set: { featured: true } }
);
 
print(EJSON.stringify({
  matchedCount: noChangeResult.matchedCount,
  modifiedCount: noChangeResult.modifiedCount
}));

结果展示

json
{"matchedCount":1,"modifiedCount":0}

结果准确表达了两件事:书存在,但目标值已经满足要求。若 matchedCount 也是 0,才说明过滤器没有找到文档。


安全删除未发布草稿

知识点

删除最危险的部分不是方法名,而是过滤条件。deleteOne({}) 会删除任意一条文档;deleteMany({}) 会清空集合。更稳妥的流程是先用完全相同的过滤器预览,再执行删除,并检查 deletedCount。

我们的删除条件同时约束 _id、发布状态和库存。即使 _id 写对了,只要草稿已经上架或有库存,操作也不会删除它。

实操

先预览目标:

javascript
const draftFilter = {
  _id: "book-draft",
  published: false,
  stock: 0
};
 
print(EJSON.stringify(db.books.findOne(
  draftFilter,
  { _id: 1, title: 1, published: 1, stock: 1 }
)));

结果展示

json
{"_id":"book-draft","title":"尚未发布的草稿","stock":0,"published":false}

预览确认目标后,再执行删除:

知识点

复用同一个 draftFilter 能避免预览条件和删除条件手工抄写不一致。deleteOne() 最多删除一个文档,返回的 deletedCount 是最终判断依据。

实操

javascript
const deleteResult = db.books.deleteOne(draftFilter);
 
print(EJSON.stringify({
  acknowledged: deleteResult.acknowledged,
  deletedCount: deleteResult.deletedCount
}));

结果展示

json
{"acknowledged":true,"deletedCount":1}

deletedCount: 1 表示草稿确实被删除。最后验证集合状态:

知识点

删除后的目标查询应返回 null,集合总数应从 5 变为 4。两个检查一起做,可以同时确认目标消失和其余书籍仍在。

实操

javascript
print(EJSON.stringify({
  deletedDocument: db.books.findOne({ _id: "book-draft" }),
  remainingBooks: db.books.countDocuments({})
}));

结果展示

json
{"deletedDocument":null,"remainingBooks":4}

纸舟书店现在有 4 本正式书籍。顾客数量因为新增陈墨变成 4,订单仍是 5 张。


检查你的理解

1
一次 `updateOne()` 返回 `matchedCount: 1`、`modifiedCount: 0`,最准确的解释是什么?

这一节结束时,CRUD 四类动作都已经落到纸舟书店的真实状态上:新增一位顾客、读取四本已发布书、售出一本 MongoDB 书、删除一份未发布草稿。下一节会组合比较、逻辑、数组与嵌套条件,并实现不会因同价书籍而抖动的稳定分页。

上一章BSON、集合与纸舟书店种子数据下一章查询、投影、排序与稳定分页