分类课程智能体AI
文章
订阅
分类课程AI导师
文章
价格
课程进度
4 / 17
上一节BSON、集合与纸舟书店种子数据下一节查询、投影、排序与稳定分页
自在学

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

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

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

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

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1

编程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 书、删除一份未发布草稿。下一节会组合比较、逻辑、数组与嵌套条件,并实现不会因同价书籍而抖动的稳定分页。

  • 先确认起点
    • 知识点
    • 实操
    • 结果展示
  • 创建一位新顾客
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
  • 读取已发布书籍
    • 知识点
    • 实操
    • 结果展示
  • 精确读取一个文档
    • 知识点
    • 实操
    • 结果展示
  • 用条件更新完成一次售卖
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
  • 理解 matchedCount 与 modifiedCount
    • 知识点
    • 实操
    • 结果展示
  • 安全删除未发布草稿
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
  • 检查你的理解

目录

  • 先确认起点
    • 知识点
    • 实操
    • 结果展示
  • 创建一位新顾客
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
  • 读取已发布书籍
    • 知识点
    • 实操
    • 结果展示
  • 精确读取一个文档
    • 知识点
    • 实操
    • 结果展示
  • 用条件更新完成一次售卖
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
  • 理解 matchedCount 与 modifiedCount
    • 知识点
    • 实操
    • 结果展示
  • 安全删除未发布草稿
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
    • 知识点
    • 实操
    • 结果展示
  • 检查你的理解