Mongoose 让操作更优雅
🎯 引言
上一篇我们用官方驱动在 Node.js 中操作了 MongoDB,查询、写入都没问题。但你可能已经注意到:MongoDB 不限制文档结构,同一个集合里每条数据的字段可以各不相同。这种灵活在小项目里是优点,在项目变大后就容易变成混乱。
学完这篇文章,你能理解 ODM 是什么,会用 Mongoose 定义 Schema 和 Model,用更优雅的方式完成增删改查,还能让数据校验自动生效,把脏数据挡在数据库门外。
🧱 为什么需要 Mongoose
MongoDB 本身不会检查你写入的数据长什么样。比如往 user 集合插数据时,一次把字段写成 username,另一次手滑写成 usernmae,MongoDB 都会照单全收;年龄一会儿是数字、一会儿是字符串,它也完全不管。
学习阶段这不算什么,但项目大了以后,这些「写乱的文档」会让查询结果变得不可预期,排查起来非常头疼。
Mongoose 就是来解决这个问题的。它是一种 ODM(Object Document Mapping,对象文档映射)工具,可以给集合加上一层「结构约束 + 数据校验」:字段叫什么、是什么类型、是否必填、取值范围,都在代码里写清楚,不符合规则的数据直接拒绝写入。
可以打个比喻:官方驱动像一本自由生长的笔记本,想写什么就写什么,没人拦你;Mongoose 像一张带表格的登记表,每一栏填什么、怎么填都有规定,填错了当场被退回来。
🛠 安装与连接
本文基于 Node.js v22 LTS、Mongoose 9.x 编写和验证,MongoDB 还是我们之前用 Docker 跑起来的 mongo-demo 容器(mongo:8,端口 27017,账号 root、密码 123456)。
docker start mongo-demo 把它启动起来。新建一个项目目录,初始化后安装 mongoose:
mkdir node-mongoose && cd node-mongoose
npm init -y
npm install mongoose
然后新建 db.js,负责连接数据库:
const mongoose = require('mongoose');
async function connect() {
await mongoose.connect(
'mongodb://root:123456@localhost:27017/blog_demo?authSource=admin'
);
console.log('MongoDB 连接成功');
}
module.exports = connect;
连接串里有三个点要注意:
localhost:27017是 Docker 容器映射出来的地址和端口。/blog_demo是直接写在连接串里的数据库名,连接成功后默认操作的就是它,不存在再创建。?authSource=admin表示去admin数据库校验账号密码。因为 root 账号是在 admin 库里创建的,少了这个参数会认证失败,这是新手连接带密码的 MongoDB 时常见的坑。
✨ 定义 Schema 和 Model
Mongoose 的两个核心概念是 Schema 和 Model:
- Schema(模式):描述集合里文档的结构,相当于前面说的「登记表的表头」。
- Model(模型):由 Schema 生成的操作入口,增删改查都通过它来完成。
我们为 user 集合定义一个 Schema:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
age: { type: Number, min: 0, max: 150 },
city: { type: String, default: '未知' },
});
const User = mongoose.model('User', userSchema);
module.exports = User;
逐个字段来看:
username:字符串类型,required: true表示必填,不写就报错。age:数字类型,min和max限制取值范围在 0 到 150 之间。city:字符串类型,default: '未知'表示不传时自动填「未知」。
mongoose.model('User', userSchema) 生成了 User 这个模型。注意首个参数 'User' 是模型名,Mongoose 会自动把它转成小写复数去对应集合,也就是操作 users 集合。这是它的约定,不用手动指定。
user 集合,注意 Mongoose 默认找的是 users。想让它继续用 user,可以显式传入第三个参数:mongoose.model('User', userSchema, 'user')。💡 用 Model 完成增删改查
有了 Model,增删改查都变得非常直白,方法名基本见名知意:
const connect = require('./db');
const User = require('./user');
async function main() {
await connect();
// 新增
const user = await User.create({ username: '小明', age: 20, city: '北京' });
console.log('新增成功:', user);
// 查询全部
const users = await User.find();
console.log('用户列表:', users);
// 按 id 查询单个用户
const found = await User.findById(user._id);
console.log('按 id 查询:', found);
// 更新
await User.updateOne({ _id: user._id }, { age: 26 });
// 删除
await User.deleteOne({ _id: user._id });
process.exit(0);
}
main();
和官方驱动对比着看:create 对应 insertOne,find 对应 find,updateOne 和 deleteOne 名字都一样。区别在于 create 会直接返回创建好的完整文档,不用再查一次;查询返回的也都是带 Schema 结构的文档对象,字段清清楚楚。
再来看数据校验怎么生效。试着创建一条缺少 username 的数据:
await User.create({ age: 20, city: '北京' });
运行后会抛出 ValidationError,错误信息里明确告诉你 username 是必填字段。同样,把 age 写成 200 也会因为超出 max: 150 被拒绝。脏数据根本进不了数据库,这就是 Schema 的价值。
⚖️ 和官方驱动怎么选
两个工具没有绝对的好坏,关键看场景:
- 官方驱动:写法更接近 MongoDB 原生语法,包更小。写一次性脚本、数据迁移工具,或者需要精细控制查询细节时,用它更轻量。
- Mongoose:自带结构定义和数据校验,还有中间件、关联查询等进阶能力。业务项目里多人协作、数据字段固定时,用它能省掉大量校验代码,结构也更清晰。
可以这么记:临时脚本用驱动,正经业务用 Mongoose。实际工作中业务项目里 Mongoose 是主流选择。
🧾 小节总结
- MongoDB 不限制文档结构,灵活但容易写乱;Mongoose 是 ODM 工具,为集合加上结构约束和数据校验。
- 安装:
npm install mongoose,连接用mongoose.connect(),连接串里直接带数据库名,带密码连接时要加authSource=admin。 - Schema 描述文档结构(类型、必填、默认值、取值范围),Model 由 Schema 生成,是增删改查的操作入口。
- 模型名
'User'会自动对应小写复数的users集合,可用第三个参数显式指定集合名。 - CRUD 用
User.create()、User.find()、User.findById()、User.updateOne()、User.deleteOne()。 - 不符合 Schema 规则的数据会抛出
ValidationError,无法写入数据库。
❓ 知识问答
Q1:ODM 和之前听过的 ORM 是什么关系?
思路是一样的,都是把代码里的对象映射到数据库。ORM 对应关系型数据库(对象映射到表和行),ODM 对应 MongoDB 这类文档数据库(对象映射到集合和文档)。
Q2:Schema 里没定义的字段还能写进去吗?
默认不能。Mongoose 会忽略 Schema 里没有声明的字段,写入时自动丢弃。这也是它帮你保持数据结构干净的一部分。
Q3:已有的集合里没有 _id 之外的约束,用 Mongoose 会报错吗?
不会。Schema 校验只发生在「通过 Mongoose 写入」时,已经存在的历史数据不受影响。查询时也能正常读出。
Q4:连接串里的 authSource=admin 能不能省略?
本课程的 Docker 环境不能省略,因为 root 账号是在 admin 库里创建的。如果你用的是专门为某个业务库创建的账号,authSource 就要填那个业务库的名字。
Q5:User.updateOne 更新时会触发 Schema 校验吗?
默认情况下 updateOne 不做完整校验(比如不会检查 required)。如果需要更新时也校验,可以传入 { runValidators: true } 选项。日常开发知道有这个开关即可。
🧪 小练习
练习一:给 userSchema 增加一个 email 字段,要求字符串类型、必填。然后创建一条带 email 的用户数据。
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
age: { type: Number, min: 0, max: 150 },
city: { type: String, default: '未知' },
// 请在这里编写代码
});
练习二:写一个 findUsersByCity 函数,接收城市名参数,用 User.find() 返回该城市的所有用户。
async function findUsersByCity(city) {
// 请在这里编写代码
}
🎉 恭喜你已经掌握用 Mongoose 优雅操作 MongoDB 的技能啦!下一篇我们聊聊索引,看看数据量大起来之后,怎样给查询提速,让 MongoDB 保持轻快。
