Node.js: Mongoose ODM
最后更新:2026-08-26
Bob 用原生 MongoDB Driver 写的数据验证代码散落在各个路由中——注册接口检查邮箱格式,发布文章接口验证标题长度,修改密码接口确认新旧密码不同。每增加一个字段,就要在对应的 controller 里加一段 if (!field) 逻辑,三个路由里重复写着相同的邮箱正则。改用 Mongoose 后,Bob 把所有验证规则集中在 Schema 定义中,一处定义处处生效,controller 代码精简了 60%。
你将学到:
- Schema / Model / Document 三层架构的关系与用法
- 字段类型、验证器(required / enum / min / max / match)的声明式定义
- 虚拟属性 virtual 的计算字段模式
- pre / post 钩子的生命周期拦截
- 查询构建器链式调用
- 索引(index / unique)与性能优化
- 实例方法与静态方法的自定义扩展
- populate 关联查询实现文档间引用
1. Mongoose 核心架构
Mongoose 是 MongoDB 的对象文档映射(ODM)库,在原生 Driver 之上提供 Schema 驱动的数据建模层。核心概念分三层:Schema 定义结构 → Model 编译为构造函数 → Document 是模型实例。
▶ 示例:(1) Schema → Model → Document 关系
graph LR
A["Schema<br/>定义结构与验证"] -->|mongoose.model() 编译| B["Model<br/>构造函数 + 查询接口"]
B -->|new Model() 实例化| C["Document<br/>带验证的文档实例"]
C -->|.save() 持久化| D[("MongoDB<br/>集合")]
B -->|Model.find() 等| D
D -->|返回| C
(2) 安装与连接
▶ 示例:安装 Mongoose 并连接 MongoDB
npm install mongoose
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/myapp')
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('Connection error:', err));
(3) 最简 Schema 与 Model
▶ 示例:定义用户 Schema 并创建 Model
const userSchema = new mongoose.Schema({
name: String,
email: String,
age: Number
});
const User = mongoose.model('User', userSchema);
| 概念 | 角色 | 类比 |
|---|---|---|
| Schema | 蓝图 / 结构定义 | 建筑图纸 |
| Model | 构造函数 + 数据库操作接口 | 施工队 |
| Document | 带验证的文档实例 | 建好的房子 |
2. Schema 字段类型
Mongoose 为每个字段提供丰富的类型映射,远超原生 Driver 的无类型约束。
(1) 字段类型速查
| Mongoose 类型 | JS 对应类型 | 示例 | 说明 |
|---|---|---|---|
String |
String | name: String |
自动 trim(需配置) |
Number |
Number | age: Number |
支持 min / max |
Boolean |
Boolean | active: Boolean |
自动转换 0/1/"true" |
Date |
Date | createdAt: Date |
内置 Date 方法 |
ObjectId |
ObjectId | author: mongoose.Schema.Types.ObjectId |
引用其他文档 |
Array |
Array | tags: [String] |
子文档数组或类型数组 |
Mixed |
Object | meta: mongoose.Schema.Types.Mixed |
任意类型,无验证 |
Buffer |
Buffer | avatar: Buffer |
二进制数据 |
Map |
Map | prefs: { type: Map, of: String } |
ES6 Map 结构 |
Decimal128 |
Decimal128 | price: mongoose.Schema.Types.Decimal128 |
高精度小数 |
(2) 字段定义的完整写法
▶ 示例:字段选项详解
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '商品名称不能为空'],
trim: true,
minlength: 2,
maxlength: 100
},
price: {
type: Number,
required: true,
min: [0, '价格不能为负数'],
default: 0
},
category: {
type: String,
enum: ['electronics', 'books', 'clothing', 'food'],
lowercase: true
},
tags: [String],
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {}
}
});
3. 验证器
验证是 Mongoose 的核心价值——将数据校验从 controller 中解放出来,集中到 Schema 声明中。
(1) 验证器速查
| 验证器 | 适用类型 | 说明 | 示例 |
|---|---|---|---|
required |
所有 | 必填字段 | required: [true, '不能为空'] |
enum |
String | 枚举值约束 | enum: ['A', 'B', 'C'] |
min |
Number / Date | 最小值 | min: 0 |
max |
Number / Date | 最大值 | max: 150 |
minlength |
String | 最小长度 | minlength: 6 |
maxlength |
String | 最大长度 | maxlength: 200 |
match |
String | 正则匹配 | match: [/^\S+@\S+\.\S+$/, '邮箱格式不正确'] |
validate |
所有 | 自定义验证函数 | validate: v => v > 0 |
(2) 自定义验证器
▶ 示例:自定义验证器与错误消息
const userSchema = new mongoose.Schema({
password: {
type: String,
required: true,
validate: {
validator: function(v) {
return /^(?=.*[A-Z])(?=.*\d).{8,}$/.test(v);
},
message: props => `${props.value} 不符合密码要求:至少8位,包含大写字母和数字`
}
},
phone: {
type: String,
validate: {
validator: function(v) {
return /^1[3-9]\d{9}$/.test(v);
},
message: '手机号格式不正确'
}
}
});
(3) 验证触发时机
验证在以下时机自动触发:new Model().save()、Model.create()。使用 validate() 方法可手动触发。updateOne() / updateMany() 等不会自动触发验证,需设置 runValidators: true 选项。
▶ 示例:更新操作启用验证
User.updateOne(
{ email: 'bob@test.com' },
{ age: -5 },
{ runValidators: true }
);
4. 虚拟属性 virtual
虚拟属性不存入数据库,只在查询时动态计算,适合派生字段。
(1) 定义与用法
▶ 示例:用户全名虚拟属性
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String,
email: String
});
userSchema.virtual('fullName')
.get(function() {
return `${this.firstName} ${this.lastName}`;
})
.set(function(v) {
const parts = v.split(' ');
this.firstName = parts[0];
this.lastName = parts[1] || '';
});
const User = mongoose.model('User', userSchema);
const user = new User({ firstName: 'Bob', lastName: 'Smith' });
console.log(user.fullName);
Bob Smith
(2) virtual 与 toJSON
虚拟属性默认不包含在 toJSON() 和 toObject() 输出中。需在 Schema 选项中显式启用:
const userSchema = new mongoose.Schema({
firstName: String,
lastName: String
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
5. 钩子(中间件)
钩子(Middleware)在文档生命周期的特定阶段自动执行,用于数据预处理、日志记录、级联操作等。
(1) 钩子类型与触发时机
| 钩子类型 | 触发时机 | 常见用途 |
|---|---|---|
pre('save') |
保存前 | 密码哈希、数据格式化、更新时间戳 |
post('save') |
保存后 | 发送通知、日志记录 |
pre('remove') |
删除前 | 级联删除关联文档 |
post('remove') |
删除后 | 清理资源、日志 |
pre('find') |
查询前 | 默认过滤条件(如软删除) |
post('find') |
查询后 | 数据脱敏 |
pre('updateOne') |
更新前 | 更新时间戳 |
post('aggregate') |
聚合后 | 日志记录 |
(2) pre 钩子
▶ 示例:保存前自动哈希密码
const bcrypt = require('bcrypt');
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
(3) post 钩子
▶ 示例:保存后发送欢迎邮件
userSchema.post('save', function(doc, next) {
console.log(`用户 ${doc.email} 已保存`);
next();
});
(4) 查询钩子
▶ 示例:查询时自动过滤已删除文档
userSchema.pre('find', function() {
this.where({ deletedAt: null });
});
6. 查询构建器
Mongoose 的查询构建器支持链式调用,比原生 Driver 的对象参数更直观。
(1) 链式查询方法
▶ 示例:查询构建器链式调用
const users = await User.find()
.where('age').gte(18).lte(65)
.where('role').equals('admin')
.sort({ createdAt: -1 })
.select('name email age')
.limit(10)
.skip(0);
console.log(users);
(2) 常用查询方法对比
| 原生 Driver 写法 | Mongoose 查询构建器 | 说明 |
|---|---|---|
db.users.find({ age: { $gte: 18 } }) |
User.find().where('age').gte(18) |
条件查询 |
db.users.find().sort({ name: 1 }) |
User.find().sort({ name: 1 }) |
排序 |
db.users.find().limit(10) |
User.find().limit(10) |
限制条数 |
db.users.find().skip(20) |
User.find().skip(20) |
跳过条数 |
db.users.find({}, { name: 1 }) |
User.find().select('name') |
字段筛选 |
(3) 分页查询封装
▶ 示例:分页查询辅助函数
async function paginate(Model, filter = {}, page = 1, limit = 10) {
const skip = (page - 1) * limit;
const [docs, total] = await Promise.all([
Model.find(filter).skip(skip).limit(limit).sort({ createdAt: -1 }),
Model.countDocuments(filter)
]);
return {
data: docs,
total,
page,
totalPages: Math.ceil(total / limit)
};
}
const result = await paginate(User, { role: 'user' }, 2, 10);
7. 索引
索引是数据库查询性能的关键。Mongoose 支持在 Schema 中声明式定义索引。
(1) 单字段索引与复合索引
▶ 示例:Schema 中定义索引
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true
},
username: {
type: String,
index: true
},
region: String,
status: String
});
userSchema.index({ region: 1, status: 1 });
| 索引类型 | 定义方式 | 说明 |
|---|---|---|
| 唯一索引 | unique: true |
字段值不可重复 |
| 普通索引 | index: true |
加速查询 |
| 复合索引 | schema.index({ a: 1, b: -1 }) |
多字段联合索引 |
| 文本索引 | schema.index({ title: 'text' }) |
全文搜索 |
▶ 示例:(2) 开发环境自动创建索引
mongoose.connect(uri, { autoIndex: true });
生产环境建议关闭 autoIndex,通过迁移脚本手动创建索引以避免启动延迟。
8. 实例方法与静态方法
Mongoose 允许在 Schema 上扩展自定义方法,分为实例方法和静态方法两类。
(1) 实例方法
实例方法操作单个文档,通过 this 访问当前文档。
▶ 示例:密码比对实例方法
userSchema.methods.comparePassword = function(candidate) {
return bcrypt.compare(candidate, this.password);
};
const user = await User.findOne({ email: 'bob@test.com' });
const isMatch = await user.comparePassword('mypassword');
(2) 静态方法
静态方法挂在 Model 上,不依赖文档实例,适合查询辅助。
▶ 示例:按角色查找静态方法
userSchema.statics.findByRole = function(role) {
return this.find({ role }).sort({ createdAt: -1 });
};
const admins = await User.findByRole('admin');
| 类型 | 定义方式 | 调用方式 | this 指向 |
|---|---|---|---|
| 实例方法 | schema.methods.xxx = function |
doc.xxx() |
Document 实例 |
| 静态方法 | schema.statics.xxx = function |
Model.xxx() |
Model |
9. populate 关联查询
Mongoose 的 populate() 实现 MongoDB 文档间的引用解析,类似 SQL 的 JOIN。
(1) 引用定义与关联查询
▶ 示例:文章关联作者
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
});
const Post = mongoose.model('Post', postSchema);
const posts = await Post.find().populate('author', 'firstName lastName email');
(2) 多层 populate 与条件筛选
▶ 示例:多层关联与筛选
const commentSchema = new mongoose.Schema({
content: String,
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }
});
const Comment = mongoose.model('Comment', commentSchema);
const comments = await Comment.find()
.populate('author', 'firstName lastName')
.populate({
path: 'post',
select: 'title content',
match: { status: 'published' }
});
(3) populate 性能注意
populate() 本质是发额外的查询再合并结果,不是真正的 JOIN。N+1 问题依然存在——100 篇文章关联 100 个不同作者,会触发 101 次查询。对于高频关联场景,考虑嵌入文档或 $lookup 聚合。
10. Mongoose vs 原生 Driver 对比
| 维度 | 原生 MongoDB Driver | Mongoose ODM |
|---|---|---|
| 数据验证 | 手写 if/else 散落在 controller | Schema 声明式验证,集中管理 |
| 类型约束 | 无,任何字段可存任何值 | Schema 强类型,自动转换 |
| 关联查询 | 手动 $lookup 聚合 |
populate() 一行搞定 |
| 生命周期钩子 | 无 | pre / post 钩子 |
| 虚拟属性 | 无 | virtual 动态计算字段 |
| 索引管理 | 手动 createIndex() |
Schema 声明 + 自动创建 |
| 查询 API | 对象参数 find({ age: { $gte: 18 } }) |
链式构建器 + 对象参数 |
| 学习曲线 | 低,贴近 MongoDB 原生语法 | 中,需理解 Schema/Model/Document |
| 灵活性 | 高,完全控制 | 中,Schema 外字段默认忽略 |
| 性能 | 略优,无中间层 | 略低,验证与钩子有开销 |
11. 综合示例:用户与文章数据模型
将 Schema、验证、钩子、虚拟属性、实例方法、关联查询组合成完整的数据模型体系。
▶ 示例:models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const userSchema = new mongoose.Schema({
firstName: {
type: String,
required: [true, '姓不能为空'],
trim: true
},
lastName: {
type: String,
required: [true, '名不能为空'],
trim: true
},
email: {
type: String,
required: [true, '邮箱不能为空'],
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '邮箱格式不正确']
},
password: {
type: String,
required: [true, '密码不能为空'],
minlength: 8,
validate: {
validator: function(v) {
return /^(?=.*[A-Z])(?=.*\d).{8,}$/.test(v);
},
message: '密码至少8位,需包含大写字母和数字'
}
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
age: {
type: Number,
min: [0, '年龄不能为负数'],
max: [150, '年龄不能超过150']
},
createdAt: {
type: Date,
default: Date.now
}
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
userSchema.methods.comparePassword = function(candidate) {
return bcrypt.compare(candidate, this.password);
};
userSchema.statics.findByRole = function(role) {
return this.find({ role }).sort({ createdAt: -1 });
};
module.exports = mongoose.model('User', userSchema);
▶ 示例:models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: [true, '标题不能为空'],
trim: true,
minlength: [2, '标题至少2个字符'],
maxlength: [200, '标题最多200个字符']
},
content: {
type: String,
required: [true, '内容不能为空']
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft'
},
tags: [{
type: String,
lowercase: true
}],
viewCount: {
type: Number,
default: 0,
min: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
postSchema.index({ status: 1, createdAt: -1 });
postSchema.index({ tags: 1 });
postSchema.virtual('excerpt').get(function() {
return this.content.substring(0, 100) + '...';
});
postSchema.pre('save', function(next) {
if (this.isModified('content')) {
this.updatedAt = new Date();
}
next();
});
postSchema.post('remove', async function(doc) {
await mongoose.model('Comment').deleteMany({ post: doc._id });
});
postSchema.statics.findPublished = function() {
return this.find({ status: 'published' })
.populate('author', 'firstName lastName email')
.sort({ createdAt: -1 });
};
module.exports = mongoose.model('Post', postSchema);
▶ 示例:查询与关联操作
const mongoose = require('mongoose');
const User = require('./models/User');
const Post = require('./models/Post');
async function main() {
await mongoose.connect('mongodb://localhost:27017/blog');
const user = await User.create({
firstName: 'Bob',
lastName: 'Smith',
email: 'bob@example.com',
password: 'Secure123',
role: 'admin',
age: 28
});
const post = await Post.create({
title: 'Mongoose 入门指南',
content: 'Mongoose 是 MongoDB 的 ODM 库,提供 Schema 驱动的数据建模...',
author: user._id,
status: 'published',
tags: ['mongodb', 'mongoose', 'nodejs']
});
const published = await Post.findPublished();
console.log(published[0].excerpt);
console.log(published[0].author.fullName);
const match = await user.comparePassword('Secure123');
console.log('Password match:', match);
await mongoose.connection.close();
}
main();
node app.js
Mongoose 是 MongoDB 的 ODM 库,提供 Schema 驱动的数据建模......
Bob Smith
Password match: true
❓ 常见问题
toJSON: { virtuals: true }。$lookup 或缓存。ValidationError,其 errors 对象包含每个字段的错误详情。可用 Object.values(err.errors).map(e => e.message) 提取所有消息,配合 Express 错误处理中间件统一返回 400。this.isModified('field') 可判断字段是否被修改。schema.add({ newField: String }) 动态添加。但已编译的 Model 不会自动更新,需重新编译或使用 schema.plugin() 扩展。推荐项目初期规划好字段结构。📖 小节
- Mongoose 核心架构的核心概念与使用方法
- Schema 字段类型的核心概念与使用方法
- 验证器的核心概念与使用方法
- 虚拟属性 virtual的核心概念与使用方法
- 钩子(中间件)的核心概念与使用方法
- 查询构建器的核心概念与使用方法
- 索引的核心概念与使用方法
- 实例方法与静态方法的核心概念与使用方法
📝 作业
- 完成本课所有代码示例,确保每个示例都能正确运行
- 修改综合示例,添加自己的扩展功能
- 查阅官方文档,找出本课未涉及的1-2个API并编写测试代码
- 思考:在实际项目中,你会如何应用本课学到的知识?
- 尝试将本课知识与前面课程的内容结合,构建一个小项目