MongoDB本身不强制校验,Mongoose的schema是第一道防线;校验规则应写入字段定义(如required、min、match、enum),配合pre('save')处理跨字段或异步逻辑,并在update操作中显式启用runValidators:true。

schema中定义校验规则最直接有效
MongoDB本身不强制校验,Mongoose的schema是第一道防线。把校验逻辑写进字段定义里,既清晰又可复用。
常见做法是用内置校验器(如required、min、max、match)或自定义validate函数:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: [true, '邮箱必填'],
match: [/^\S+@\S+\.\S+$/, '邮箱格式不合法']
},
age: {
type: Number,
min: [0, '年龄不能小于0'],
max: [150, '年龄不能大于150']
},
status: {
type: String,
enum: ['active', 'inactive', 'pending'],
default: 'pending'
}
});
-
required是布尔值或数组,后者可传自定义错误消息 -
match接收正则或[正则, 错误消息]数组,注意正则字面量不能带/g标志(会干扰Mongoose内部匹配) -
enum校验仅在save()时触发,插入时绕过schema(如用insertMany({ raw: true }))会失效
使用pre('save')做跨字段或异步校验
内置校验器无法处理字段间依赖(比如“结束时间必须晚于开始时间”)或需查库判断的场景(如“用户名不能重复”),这时要用中间件。
pre('save')钩子在保存前执行,支持async/await:
userSchema.pre('save', async function(next) {
if (this.isModified('email')) {
const existing = await this.constructor.findOne({ email: this.email });
if (existing && existing._id.toString() !== this._id.toString()) {
return next(new Error('邮箱已被注册'));
}
}
if (this.startDate && this.endDate && this.startDate >= this.endDate) {
return next(new Error('结束时间必须晚于开始时间'));
}
next();
});
- 务必用
this.isModified('field')避免无谓查询,尤其对高频更新字段 - 异步校验必须调用
next()或return next(err),漏掉会导致save()永远挂起 - 不要在
pre('save')里改this字段再调validate()——Mongoose不会自动重跑校验,得手动调this.validate()
校验失败时的错误结构和捕获方式
Mongoose校验失败抛出ValidationError,它不是普通Error,而是含errors对象的特殊实例,字段级错误藏在errors['fieldName']里。
直接console.log(err)看不出结构,建议这样调试:
try {
await user.save();
} catch (err) {
if (err.name === 'ValidationError') {
Object.values(err.errors).forEach(e => console.log(e.message));
// 输出:'邮箱格式不合法'、'年龄不能小于0'
}
}
-
err.errors是{ fieldName: { name, message, kind, path, value } }结构,kind告诉你触发的是required还是min - HTTP接口返回时别直接
res.status(400).json({ error: err.message })——err.message只是汇总字符串(如"Validation failed: email: 邮箱格式不合法"),前端难解析 - 生产环境应提取
Object.values(err.errors).map(e => e.message)组装成明确的错误列表
注意校验只在save()和create()时运行
updateOne()、findOneAndUpdate()等更新操作默认跳过schema校验,除非显式开启runValidators: true。
User.updateOne(
{ _id: userId },
{ $set: { email: 'invalid' } },
{ runValidators: true } // 不加这行,email格式校验完全不触发
);
-
runValidators: true只校验被更新的字段,未出现在$set里的字段不会被检查 -
updateMany()也支持runValidators,但要注意它不触发pre('save')钩子——只有save()系列方法才走完整中间件链 - 批量插入
insertMany()默认不校验,加{ validateBeforeSave: true }才启用,且失败时整个操作回滚(除非设ordered: false)
校验逻辑分散在schema定义、pre钩子、更新选项三处,漏掉任何一环都可能让脏数据入库。尤其注意update类操作的runValidators开关,这是线上最容易被忽略的点。











