分类课程智能体AI
文章
订阅
分类课程AI导师
文章
价格
课程进度
3 / 17
上一节启动 MongoDB 8.0下一节用 CRUD 推进纸舟书店业务
自在学

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

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

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

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

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

编程MongoDB 纸舟书店完整课程BSON、集合与纸舟书店种子数据

BSON、集合与纸舟书店种子数据

MongoDB 已经在运行,但 bookstore 目前还是空的。这一节会先理解 BSON、文档和集合的边界,然后一次性导入全课共用的书籍、顾客与订单。后面章节都从这份固定状态继续,因此这里会使用稳定、可读的 _id,并让每次导入都有明确计数。

本节的初始化脚本会删除 books、customers、orders 三个同名集合,再重新创建并导入种子数据。它只适合课程初始化与重置,不能对包含业务数据的数据库直接执行。


从 JSON 到 BSON

JSON 很适合网络传输,但它的类型范围较窄。MongoDB 在存储和通信时使用 BSON。BSON 保留文档的键值结构,同时支持更多数据库需要的类型,例如日期、二进制、ObjectId、Decimal128、32 位整数和 64 位整数。

下面是一张订单在 JavaScript 语法中的样子:

javascript
{
  _id: "order-1001",
  customerId: "customer-lin",
  status: "paid",
  items: [
    {
      bookId: "book-mongodb",
      title: "MongoDB 从入门到实践",
      unitPrice: 89,
      quantity: 1
    }
  ],
  total: 158,
  createdAt: ISODate("2026-07-01T10:00:00Z")
}

这里有四类值得分清的值:

  • _id、customerId、status 是字符串。
  • items 是数组,数组元素又是嵌套文档。
  • unitPrice、quantity、total 是数值。
  • createdAt 是 BSON 日期,不是长得像日期的普通字符串。

课程中的价格使用整数元,是为了让结果容易核对。真实支付系统通常选择“最小货币单位的整数”或 Decimal128,并明确舍入规则;直接使用二进制浮点数保存需要精确对账的金额,容易埋下精度问题。


连接到 bookstore 数据库

知识点

mongosh 启动后会持有一个名为 db 的数据库对象。use bookstore 会把这个对象切换到 bookstore。MongoDB 不要求先执行单独的“创建数据库”命令;第一次在该数据库中创建集合或写入数据时,它才真正出现在数据库列表中。

实操

先进入容器中的 mongosh:

shell
docker exec -it paperboat-mongo mongosh --quiet

进入 shell 后执行:

javascript
use bookstore
db.getName()

后续标为 javascript 的数据库操作都在这个 shell 会话中粘贴执行。

结果展示

text
switched to db bookstore
bookstore

第一行确认切换成功,第二行来自 db.getName()。如果第二行不是 bookstore,先重新执行 use bookstore,不要把种子数据写进默认数据库。


创建三个核心集合

知识点

MongoDB 在第一次插入时可以隐式创建集合,但显式 createCollection() 允许我们在写入前配置校验规则。纸舟书店只为 orders 设置第一版校验器:订单必须有顾客、状态、至少一个明细、总额和创建时间;状态只能是四个允许值之一。

这个校验器没有列出书店未来可能增加的每一个字段。它保护不可缺少的业务事实,同时保留增加配送信息、备注等字段的空间。这就是“受控的灵活模式”。

实操

确认当前数据库是 bookstore 后,粘贴整段脚本:

javascript
for (const name of ["books", "customers", "orders"]) {
  db.getCollection(name).drop();
}
 
db.createCollection("books");
db.createCollection("customers");
 
db.createCollection("orders", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["customerId", "status", "items", "total", "createdAt"],
      properties: {
        customerId: { bsonType: "string" },
        status: {
          enum: ["pending", "paid", "shipped", "cancelled"]
        },
        items: {
          bsonType: "array",
          minItems: 1,
          items: {
            bsonType: "object",
            required: ["bookId", "title", "unitPrice", "quantity"],
            properties: {
              bookId: { bsonType: "string" },
              title: { bsonType: "string" },
              unitPrice: {
                bsonType: ["int", "long", "double", "decimal"]
              },
              quantity: { bsonType: ["int", "long"] }
            }
          }
        },
        total: {
          bsonType: ["int", "long", "double", "decimal"]
        },
        createdAt: { bsonType: "date" }
      }
    }
  }
});
 
print(EJSON.stringify(db.getCollectionNames().sort()));

结果展示

json
["books","customers","orders"]

drop() 在集合不存在时会返回 false,存在时返回 true。脚本没有把这些布尔值当成验收结果,因为首次运行和重置运行必然不同;最后的集合名称才是稳定检查点。


导入书籍文档

知识点

insertMany() 在一次调用中插入多个文档,并返回 acknowledged 与 insertedIds。我们用字符串 _id 保证每本书都能被稳定引用。实际项目可以使用自动生成的 ObjectId,但不要把“自动生成”误解为“业务上不需要标识策略”。

五本文档有意覆盖后续查询需要的差异:两个数据库类别、不同价格和库存、数组标签、四本已发布书和一份待删除草稿。

实操

javascript
const booksResult = db.books.insertMany([
  {
    _id: "book-mongodb",
    title: "MongoDB 从入门到实践",
    category: "数据库",
    price: 89,
    stock: 20,
    tags: ["MongoDB", "后端"],
    published: true
  },
  {
    _id: "book-node",
    title: "Node.js 项目开发",
    category: "编程",
    price: 69,
    stock: 15,
    tags: ["Node.js", "后端"],
    published: true
  },
  {
    _id: "book-design",
    title: "数据建模的艺术",
    category: "数据库",
    price: 79,
    stock: 8,
    tags: ["建模", "架构"],
    published: true
  },
  {
    _id: "book-web",
    title: "现代 Web 基础",
    category: "编程",
    price: 59,
    stock: 30,
    tags: ["Web", "前端"],
    published: true
  },
  {
    _id: "book-draft",
    title: "尚未发布的草稿",
    category: "草稿",
    price: 0,
    stock: 0,
    tags: [],
    published: false
  }
]);
 
print(EJSON.stringify({
  acknowledged: booksResult.acknowledged,
  insertedCount: Object.keys(booksResult.insertedIds).length
}));

结果展示

json
{"acknowledged":true,"insertedCount":5}

acknowledged: true 表示服务端确认了写入,insertedCount: 5 表示五本文档都获得了插入结果。因为 _id 固定,跳过前面的重置脚本直接重复插入会触发重复键错误;这是保护唯一标识的正常行为。


导入顾客文档

知识点

顾客的地址随顾客资料一起读取,数量也通常有限,因此这里把地址数组嵌入顾客文档。数组中的每个元素仍是完整文档,可以拥有 label、city 和 detail 字段。

如果地址以后变成独立管理、被多个账户共享、拥有复杂生命周期的实体,我们可以重新评估是否拆成引用。当前模型服务的是当前访问模式,不是假设永远不变。

实操

javascript
const customersResult = db.customers.insertMany([
  {
    _id: "customer-lin",
    name: "林晓舟",
    email: "lin@example.com",
    addresses: [
      {
        label: "默认",
        city: "上海",
        detail: "静安区纸舟路 8 号"
      }
    ]
  },
  {
    _id: "customer-zhou",
    name: "周雨",
    email: "zhou@example.com",
    addresses: [
      {
        label: "默认",
        city: "杭州",
        detail: "西湖区云栖街 12 号"
      }
    ]
  },
  {
    _id: "customer-wang",
    name: "王青",
    email: "wang@example.com",
    addresses: [
      {
        label: "默认",
        city: "成都",
        detail: "锦江区书香巷 5 号"
      }
    ]
  }
]);
 
print(EJSON.stringify({
  acknowledged: customersResult.acknowledged,
  insertedCount: Object.keys(customersResult.insertedIds).length
}));

结果展示

json
{"acknowledged":true,"insertedCount":3}

三位顾客写入成功。地址没有单独的 _id,因为课程暂时只在所属顾客内部定位它;顶层顾客则有稳定 _id,供订单引用。


导入订单文档

知识点

订单同时使用引用与快照:customerId 指向顾客,items.bookId 指向书籍;items.title 和 items.unitPrice 保存下单时的书名与单价。以后书籍调价,历史订单的 total 不会跟着改变。

订单时间使用 ISODate() 创建 BSON 日期。末尾的 Z 表示 UTC。把时间明确存成日期后,MongoDB 才能正确进行范围查询、排序和日期聚合。

实操

javascript
const ordersResult = db.orders.insertMany([
  {
    _id: "order-1001",
    customerId: "customer-lin",
    status: "paid",
    items: [
      {
        bookId: "book-mongodb",
        title: "MongoDB 从入门到实践",
        unitPrice: 89,
        quantity: 1
      },
      {
        bookId: "book-node",
        title: "Node.js 项目开发",
        unitPrice: 69,
        quantity: 1
      }
    ],
    total: 158,
    createdAt: ISODate("2026-07-01T10:00:00Z")
  },
  {
    _id: "order-1002",
    customerId: "customer-zhou",
    status: "paid",
    items: [
      {
        bookId: "book-design",
        title: "数据建模的艺术",
        unitPrice: 79,
        quantity: 2
      }
    ],
    total: 158,
    createdAt: ISODate("2026-07-02T10:00:00Z")
  },
  {
    _id: "order-1003",
    customerId: "customer-lin",
    status: "shipped",
    items: [
      {
        bookId: "book-mongodb",
        title: "MongoDB 从入门到实践",
        unitPrice: 89,
        quantity: 2
      }
    ],
    total: 178,
    createdAt: ISODate("2026-07-03T10:00:00Z")
  },
  {
    _id: "order-1004",
    customerId: "customer-wang",
    status: "pending",
    items: [
      {
        bookId: "book-web",
        title: "现代 Web 基础",
        unitPrice: 59,
        quantity: 3
      }
    ],
    total: 177,
    createdAt: ISODate("2026-07-04T10:00:00Z")
  },
  {
    _id: "order-1005",
    customerId: "customer-zhou",
    status: "paid",
    items: [
      {
        bookId: "book-mongodb",
        title: "MongoDB 从入门到实践",
        unitPrice: 89,
        quantity: 1
      },
      {
        bookId: "book-design",
        title: "数据建模的艺术",
        unitPrice: 79,
        quantity: 1
      }
    ],
    total: 168,
    createdAt: ISODate("2026-07-05T10:00:00Z")
  }
]);
 
print(EJSON.stringify({
  acknowledged: ordersResult.acknowledged,
  insertedCount: Object.keys(ordersResult.insertedIds).length
}));

结果展示

json
{"acknowledged":true,"insertedCount":5}

五张订单通过校验并写入。订单总额是下单事实,不会在查询时根据书籍当前价格临时重算。


验证数量与 BSON 日期

知识点

导入成功不能只看“没有报错”。我们需要核对三个集合的文档数,并验证订单时间确实是 Date。countDocuments({}) 是明确的文档计数方法;不要使用已经废弃的 count()。

实操

javascript
const sampleOrder = db.orders.findOne({ _id: "order-1001" });
 
print(EJSON.stringify({
  counts: {
    books: db.books.countDocuments({}),
    customers: db.customers.countDocuments({}),
    orders: db.orders.countDocuments({})
  },
  sample: {
    id: sampleOrder._id,
    createdAtIsDate: sampleOrder.createdAt instanceof Date,
    itemCount: sampleOrder.items.length
  }
}));

结果展示

json
{"counts":{"books":5,"customers":3,"orders":5},"sample":{"id":"order-1001","createdAtIsDate":true,"itemCount":2}}

数量与种子设计一致;createdAtIsDate: true 证明它不是普通字符串;itemCount: 2 证明订单明细数组保留了两个嵌套文档。


观察校验器拒绝错误订单

知识点

集合校验器在服务端执行。无论写入来自 mongosh、应用驱动还是导入工具,只要没有被明确授权绕过,错误文档都会受到同一规则约束。

下面的文档缺少 customerId,并且 items 是空数组,同时违反两条规则。我们用 try...catch 只打印稳定的错误码,避免把很长且可能随补丁版本变化的错误详情当作固定输出。

实操

javascript
try {
  db.orders.insertOne({
    _id: "order-invalid",
    status: "paid",
    items: [],
    total: 0,
    createdAt: new Date()
  });
} catch (error) {
  print("INVALID_REJECTED code=" + error.code);
}
 
print("invalidCount=" + db.orders.countDocuments({ _id: "order-invalid" }));

结果展示

text
INVALID_REJECTED code=121
invalidCount=0

错误码 121 表示文档校验失败。invalidCount=0 再次确认错误订单没有偷偷进入集合。错误详情会指出缺少 customerId,并且 items 没有满足 minItems: 1。

现在纸舟书店拥有一个可重复的初始状态:5 本书、3 位顾客、5 张订单,订单集合还有第一道服务端校验。后面的结果都以这份状态为起点。


检查你的理解

1
订单明细为什么同时保存 `bookId`、`title` 和 `unitPrice`?
  • 从 JSON 到 BSON
  • 连接到 bookstore 数据库
    • 知识点
    • 实操
    • 结果展示
  • 创建三个核心集合
    • 知识点
    • 实操
    • 结果展示
  • 导入书籍文档
    • 知识点
    • 实操
    • 结果展示
  • 导入顾客文档
    • 知识点
    • 实操
    • 结果展示
  • 导入订单文档
    • 知识点
    • 实操
    • 结果展示
  • 验证数量与 BSON 日期
    • 知识点
    • 实操
    • 结果展示
  • 观察校验器拒绝错误订单
    • 知识点
    • 实操
    • 结果展示
  • 检查你的理解

目录

  • 从 JSON 到 BSON
  • 连接到 bookstore 数据库
    • 知识点
    • 实操
    • 结果展示
  • 创建三个核心集合
    • 知识点
    • 实操
    • 结果展示
  • 导入书籍文档
    • 知识点
    • 实操
    • 结果展示
  • 导入顾客文档
    • 知识点
    • 实操
    • 结果展示
  • 导入订单文档
    • 知识点
    • 实操
    • 结果展示
  • 验证数量与 BSON 日期
    • 知识点
    • 实操
    • 结果展示
  • 观察校验器拒绝错误订单
    • 知识点
    • 实操
    • 结果展示
  • 检查你的理解