Node.js 驱动与 Mongoose
这一课你将能做到
- 用官方
mongodb 驱动完成连接与 insertOne / find,理解连接生命周期(connect / close)
- 用 Mongoose 定义 Schema 与 Model,利用 required / enum 校验脏数据,用
lean() 优化只读查询
- 对照 Spring Data MongoDB 的概念表,说清 Node 直连在本站架构中的位置:脚本 / BFF,不是主 API 替代品
为什么要学
全栈同学常在 Node 侧写数据导入脚本、定时任务或轻量 BFF。这里有两层工具:官方驱动(mongodb 包)给最底层的 CRUD API,适合脚本与精细控制;Mongoose 在其上加 Schema 层,把「约定字段形状」写进代码——相当于应用层 schema。
本课的意义不止 Node:通过对比两种风格(裸驱动 vs ODM),你会同时理解 Spring 里 MongoTemplate 与 @Document 实体的分工——同一套思想,两种语言。
人话:驱动 = 拿着 Mongo 的遥控器直接按键;Mongoose = 先写好「数据长什么样」的模板,不合格的数据进不来。
先建立图像
Next.js platform → Spring Boot API → MySQL(权威)
↓ 可选
Node 脚本 / BFF → MongoDB(导入、日志、实验)
├─ mongodb 驱动:底层 API,脚本用
└─ Mongoose:Schema/Model,业务用
概念对照:
| Node.js | Spring Boot | mongosh |
|---|
Schema 字段定义 | @Document 实体字段 | (无强制 schema) |
mongoose.model() | MongoRepository | db.集合 |
.find() / .create() | findBy… / save | find / insertOne |
.populate() | $lookup / @DBRef | $lookup |
| 连接池(驱动内置) | 自动装配客户端 | 单连接会话 |
跟着做
步骤 1:准备项目与依赖
mkdir mongo-node-lab && cd mongo-node-lab
npm init -y
npm install mongodb mongoose
# package.json 里加 "type": "module" 以支持 ESM 语法
确认 Docker 里 Mongo 已启动,URI 复用前面课的:
mongodb://admin:secret@127.0.0.1:27017/coderoadmap_lab?authSource=admin
真实项目把 URI 放进 .env(MONGO_URI=...),代码里 process.env.MONGO_URI——下面演示为了可复制仍写明文,提交前必须替换。
步骤 2:官方驱动最小 CRUD
import { MongoClient } from "mongodb";
const uri = "mongodb://admin:secret@127.0.0.1:27017/coderoadmap_lab?authSource=admin";
const client = new MongoClient(uri);
async function main() {
await client.connect();
const col = client.db().collection("lesson_logs"); // URI 里已指定库
const r = await col.insertOne({
lessonSlug: "mongo-node",
userId: "u_1001",
action: "view",
at: new Date()
});
console.log("inserted:", r.insertedId);
const list = await col.find({ userId: "u_1001" })
.sort({ at: -1 })
.limit(5)
.toArray();
console.log(list);
}
main()
.catch((e) => { console.error(e); process.exitCode = 1; })
.finally(() => client.close()); // 脚本必须收尾关连接
应看到:打印 inserted 的 ObjectId 与文档数组;mongosh 里 find 能对上。注意 find() 返回游标,必须 .toArray() 或迭代才拿到数据。
步骤 3:Mongoose Schema 与 Model
import mongoose from "mongoose";
await mongoose.connect(
"mongodb://admin:secret@127.0.0.1:27017/coderoadmap_lab?authSource=admin"
);
const logSchema = new mongoose.Schema({
lessonSlug: { type: String, required: true, index: true },
userId: { type: String, required: true },
action: { type: String, enum: ["view", "complete"] },
at: { type: Date, default: Date.now }
}, { versionKey: false });
// 第三个参数显式指定集合名,避免默认复数规则改名
const LessonLog = mongoose.model("LessonLog", logSchema, "lesson_logs");
await LessonLog.create({
lessonSlug: "mongo-node",
userId: "u_1002",
action: "view"
});
const list = await LessonLog
.find({ userId: "u_1002" })
.sort({ at: -1 })
.lean(); // 只读场景返回普通对象,跳过 Document 封装,更快更省
console.log(list);
await mongoose.disconnect();
步骤 4:故意触发校验错误
把 action: "view" 改成 "click" 再跑:
ValidationError: LessonLog validation failed: action: `click` is not a valid enum value
required、enum、type 都在写入/更新前拦截脏数据——这正是 ODM 相对裸驱动的核心价值:schema 约束落在代码里,评审有据可查。
步骤 5:与 Spring 实体对齐字段契约
打开 Spring 课的 LessonLog 实体,逐字段对照本课 Schema:lessonSlug、userId、action、at。两端读写同一集合时,字段名与类型完全一致比谁先写入更重要——这就是跨语言协作的「字段契约」。
步骤 6:写清架构边界三句话
- 前端 platform 走 Spring REST API,浏览器绝不直连 Mongo(URI 与凭据只活在服务端)
- Node 直连适用于脚本、迁移、内部工具、轻量 BFF
- 权威课程数据仍在 MySQL
cf_track,任何语言都不例外
讲清楚要点
- 驱动与 Mongoose 是两层:驱动给 API(脚本、一次性任务),Mongoose 给 schema 约束(业务模型);同一项目混用可以,但集合的字段契约必须唯一。
- 连接生命周期自己负责:脚本要
finally 里 close()/disconnect();常驻服务用全局单例连接(驱动内置连接池),不要每请求新建。
- Mongoose 默认复数化集合名:
model("LessonLog") 默认找 lessonlogs——第三个参数显式指定 lesson_logs 是防漂移的关键一行。
lean() 换性能:只读列表跳过 Document 封装与 change tracking;需要 save 回写的文档别 lean。
- 校验在应用层:
required/enum 拦的是应用写入;mongosh 或其他语言直写不受它管——数据库层的唯一约束仍要靠索引(如唯一索引)。
- 与 Spring 共享集合靠契约:字段名、类型、枚举值三对齐,任何一端单独改字段都是破坏性变更,要走评审。
你可能会懵的地方
-
集合里已有异构文档,Mongoose 会怎样?
读出来的文档缺字段就是 undefined、类型不符会按 strict 模式处理;Schema 只约束「经过 Model 的读写」。所以先有数据再写 Schema 时要 strict: false 或逐字段核对。
-
装了 mongoose 还要装 mongodb 吗?
mongoose 内部依赖官方驱动会自动装上;想直接用底层 API 才显式 npm install mongodb。两个 demo 分开体验即可。
-
populate() 是 JOIN 吗?
是应用层关联填充:按存的 _id 再查一次另一集合拼回来。本质多次查询(或驱动聚合优化),滥用同样有 N+1 问题——与 Spring 的 $lookup 决策逻辑一致,优先内嵌。
-
为什么示例代码里写明文密码?
为了本课可复制。真实项目必须 .env + dotenv,.env 进 .gitignore;泄露到 git 的密码即使删除提交也视为已泄露,要立刻轮换。
-
Express 服务里怎么组织连接?
启动时 mongoose.connect() 一次,全局复用默认连接;关闭时在 SIGTERM 里 disconnect。不要每个路由里 connect。
错法对照
| 错法 | 问题 | 正解 |
|---|
脚本跑完不 close()/disconnect() | 进程挂起、连接堆积 | finally 收尾关闭 |
| 依赖 model 默认集合名 | 得到 lessonlogs 而非 lesson_logs | 第三个参数显式指定 |
| Schema 字段名与 Spring 端不一致 | 两端各写各的,数据互相看不见 | 先定契约再写代码 |
| 前端 / 浏览器拿到带密码的 URI | 凭据泄露 + 任意读写 | URI 只在服务端;用户走 API |
| 关掉校验「图省事」 | 脏数据堆积、统计失真 | 保留 required / enum |
每个请求 new MongoClient + connect | 连接风暴 | 全局单例 + 驱动连接池 |
卡点急救
- 现象:脚本执行完进程不退出
连接没关。把后续逻辑放
main().finally(() => client.close());Mongoose 用 mongoose.disconnect()。常驻服务则相反——不要关。
- 现象:Model 查不到 mongosh 明明有的数据
三查:集合名是否被复数化改写(打印
LessonLog.collection.name);库名对不对(URI 里 /coderoadmap_lab);字段名是否与库里一致。逐项打印核对。
- 现象:写入报 ValidationError 但值看起来合法
检查 enum 列表是否遗漏该值、类型是否匹配(字符串 vs 数字)、是否触发了自定义 validator;先
doc.validate() 单独定位是哪个字段。
面试常问
-
Mongoose 的作用是什么?不用它直接用驱动行不行?
Mongoose 是 ODM:Schema 定义形状、应用层校验、Model 封装读写、中间件(钩子)与 populate 关联。裸驱动更轻更灵活,适合脚本与追求极致控制的场景;业务模型复杂时 ODM 的约束与可维护性收益更大——两层怎么选取决于项目形态。
-
Node 侧如何管理 MongoDB 连接?
驱动内置连接池:常驻服务在启动时建立一次连接并全局复用(mongoose 的默认连接即单例);脚本任务用完在 finally 关闭。反模式是每请求新建连接——握手开销与句柄耗尽都是线上事故来源。
-
Mongoose Schema 和 MongoDB 的灵活 schema 矛盾吗?
不矛盾,是分层:库层不强制( mongosh / 其他语言可自由写入),应用层通过 Schema 收紧本服务的写入形状。这是「库层灵活、应用层强约定」原则在 Node 生态的落地——面试答出分层即可。
自测三问
- 官方驱动与 Mongoose 各适合什么场景?
mongoose.model("LessonLog", schema, "lesson_logs") 第三个参数为什么必须写?
- Node 直连 MongoDB 在本站架构中的推荐位置是什么?
小结一句话
mongodb 驱动给底层 CRUD(脚本利器),Mongoose 用 Schema/Model 约束数据形状(业务利器);二者通过字段契约与 Spring 端共享集合,但不改变 MySQL 权威主路径。
延伸