Mongoose ODM:MongoDB 的 Node.js 对象建模
Mongoose 入门:定义数据模型与验证
Mongoose 是 Node.js 生态中最流行的 MongoDB 对象建模工具(ODM)。它通过模式(Schema)定义数据形状、内置类型转换、验证、查询构建和业务逻辑钩子,让开发者以面向对象的方式操作 MongoDB,极大提升开发效率和代码可维护性。本教程将带你从零掌握 Mongoose 核心用法。
环境准备与连接数据库
在开始之前,确保已安装 Node.js 和 MongoDB 实例(本地或 Atlas 云服务)。创建项目并安装 Mongoose:
npm init -y
npm install mongoose
建立数据库连接。建议将连接逻辑封装在独立文件中:
// db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect('mongodb://localhost:27017/myapp');
console.log('MongoDB 连接成功');
} catch (err) {
console.error('数据库连接失败', err);
process.exit(1);
}
};
module.exports = connectDB;
连接字符串可包含认证信息、副本集等配置,生产环境应将敏感信息置于环境变量。
定义模式(Schema)与模型(Model)
模式是 Mongoose 的骨架。它定义了集合中文档的结构、默认值、验证规则等。基于模式编译出模型,即可对集合进行 CRUD 操作。
基本模式定义
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, '用户名必填'],
trim: true,
maxlength: [20, '用户名不能超过20个字符'],
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址'],
},
age: {
type: Number,
min: 0,
max: 120,
},
createdAt: {
type: Date,
default: Date.now,
},
});
常用模式类型包括:String、Number、Date、Buffer、Boolean、ObjectId(引用)、Array 等。每个字段可配置 required、default、validate 等选项。
创建模型
const User = mongoose.model('User', userSchema);
module.exports = User;
mongoose.model() 第一个参数是模型名称,Mongoose 会自动将其转换为小写复数形式的集合名(例如 User → users)。可以通过第三个参数显式指定集合名。
数据验证与自定义校验
Mongoose 提供丰富的内置验证器,同时也支持自定义验证逻辑。
const productSchema = new mongoose.Schema({
price: {
type: Number,
required: true,
min: [0, '价格不能为负数'],
},
tags: {
type: [String],
validate: {
validator: function (v) {
return v.length <= 5;
},
message: '标签数量最多5个',
},
},
discount: {
type: Number,
validate: {
validator: function (value) {
// 折扣不能高于原价,这里通过 this 访问同一文档的其他字段
return value <= this.price;
},
message: '折扣必须小于等于原价',
},
},
});
this 仅在创建新文档时指向当前文档,更新操作时需特别注意。复杂异步验证可以使用 validator 返回 Promise。
CRUD 操作实战
模型提供了创建、读取、更新、删除等方法,且都返回 Promise,可与 async/await 无缝协作。
创建文档
const newUser = new User({ name: '张三', email: 'zhangsan@example.com', age: 25 });
await newUser.save();
// 或使用 Model.create
const user = await User.create({ name: '李四', email: 'lisi@example.com' });
save() 会触发验证和中间件,而 create() 等同于 new Model() + save()。
查询文档
// 查找所有用户
const users = await User.find();
// 条件查询,返回单个文档
const user = await User.findOne({ name: '张三' });
// 按 ID 查找
const userById = await User.findById('507f191e810c19729de860ea');
// 使用比较操作符
const adults = await User.find({ age: { $gte: 18 } });
// 字段筛选、分页与排序
const results = await User.find()
.select('name email -_id') // 包含 name、email,排除 _id
.limit(10)
.skip(20)
.sort({ createdAt: -1 });
更新文档
// 更新单个文档并返回更新后的文档(默认返回旧文档)
const updatedUser = await User.findOneAndUpdate(
{ _id: userId },
{ $set: { age: 26 } },
{ new: true, runValidators: true } // runValidators 开启更新验证
);
// 批量更新多个匹配文档
await User.updateMany({ age: { $lt: 18 } }, { $set: { status: 'minor' } });
删除文档
await User.deleteOne({ _id: userId });
await User.deleteMany({ status: 'inactive' });
// findOneAndDelete 返回被删除的文档
const deleted = await User.findOneAndDelete({ _id: userId });
中间件(Hook)与业务逻辑
Mongoose 中间件(前置 pre / 后置 post)允许在特定操作之前或之后执行自定义逻辑,例如数据加密、日志记录、级联删除等。
userSchema.pre('save', async function (next) {
// 如果密码字段被修改,则进行哈希处理
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 12);
}
next();
});
userSchema.post('findOneAndDelete', async function (doc) {
// 用户被删除后,清理关联数据
if (doc) {
await Post.deleteMany({ author: doc._id });
}
});
中间件针对以下操作有效:save、validate、remove、updateOne、deleteOne、find 等。需注意:updateOne 和 deleteOne 的中间件默认不绑定 this,需通过查询对象操作。
关联与引用(Population)
Mongoose 通过 ref 实现集合之间的引用关系,并使用 populate 自动替换引用字段为实际文档。
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // 指向 User 模型
required: true,
},
comments: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment',
}],
});
const Post = mongoose.model('Post', postSchema);
// 查询时 populate
const posts = await Post.find().populate('author', 'name email').populate('comments');
populate 支持多级、条件筛选和字段选择。虽然方便,但需注意查询性能,避免深层嵌套导致大量数据库查询。
查询构建与索引优化
Mongoose 查询对象链式调用最终通过 exec() 执行,也可直接使用回调或 Promise。为了提升查询性能,务必根据查询模式创建索引。
// 在 Schema 级别定义索引
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text', bio: 'text' }); // 文本索引
// 复合索引
postSchema.index({ author: 1, createdAt: -1 });
索引会加速查询,但会占用存储并降低写入速度,应根据实际业务权衡。
常见错误处理与调试
- 连接错误:检查 MongoDB 服务状态和连接字符串。
- 验证错误:捕获
ValidationError,其errors属性包含详细错误列表。 - 唯一索引冲突:错误码
11000表示重复键。 - 异步操作未捕获:务必将所有异步操作置于
try/catch中或使用.catch()。
开启 Mongoose 调试模式有助于开发:mongoose.set('debug', true),它会在控制台输出所有集合操作的原始 MongoDB 查询。
总结与最佳实践
- 严格定义 Schema:利用类型、验证和默认值保证数据一致性。
- 善用中间件:将重复逻辑(如时间戳、加密)封装到中间件中。
- 避免大文档:嵌套子文档适度,对于可能无限增长的数组(如评论)宜采用引用。
- 索引策略:根据查询模式创建索引,定期使用
explain()分析查询。 - 连接池管理:默认连接池大小为 100,可根据负载调整。
- 区分
save与findOneAndUpdate的验证行为:更新操作默认不运行 Schema 验证,需显式设置runValidators: true。 - 使用环境变量:永远不要在代码中硬编码凭据。
Mongoose 降低了 Node.js 与 MongoDB 交互的复杂度,掌握它的基本模式、验证、中间件和 population 技术,能够让你快速构建稳健的数据驱动应用。随着深入,你还可以探索插件系统、聚合管道、事务等高级特性,进一步发挥 Mongoose 的威力。