save()未触发中间件,主因是误用model级更新方法(如updateone、findoneandupdate)或未正确创建document实例;正确做法是先findbyid再调用实例save(),且需确保中间件中正确return或await。

save() 没触发中间件,大概率是因为你根本没调用 Document 实例的 save(),而是直接用了 Model 的 updateOne()、findOneAndUpdate() 或其他查询级更新方法。
你是不是在用 updateOne() 代替 save()?
这是最常见也最容易被忽略的绕过点:updateOne() 是模型(Model)上的静态方法,它直接发命令到 MongoDB,完全不经过 Schema 中定义的 pre.save() 或 post.save() 中间件。
即使你传了 { _id: xxx } 去匹配单个文档,它也不会加载该文档为 Document 实例,更不会走校验、默认值填充、字段变更监听等逻辑。
- ✅ 正确触发中间件:先
findById()得到 Document,改字段后调用doc.save() - ❌ 绕过中间件:直接
Model.updateOne({ _id }, { $set: { ... } }) - ⚠️ 特别注意:
findOneAndUpdate()默认也不触发save中间件,除非显式启用runValidators: true,但它仍不触发pre.save()—— 因为根本没有 Document 实例参与
你有没有误把 Model 当成 Document 调用 save()?
save() 只能被 Document 实例调用。如果你写的是 MyModel.save()(比如在路由里直接对 Model 调用),Node.js 会报错 MyModel.save is not a function;但更隐蔽的情况是:你本意想 new 一个实例,却漏掉了 new 关键字,或误用 create() 后没等结果就操作了原始数据。
- ❌ 错误写法:
const doc = MyModel({ name: 'a' }); doc.save()—— 这里doc是 plain object,不是 Document 实例 - ✅ 正确写法:
const doc = new MyModel({ name: 'a' }); await doc.save() - ✅ 或:
const doc = await MyModel.create({ name: 'a' })——create()内部会调用save(),所以中间件照常触发
你是否在批量操作中跳过了中间件?
像 insertMany()、bulkWrite() 这类方法,默认都不执行 pre.save()。Mongoose 从 v6 开始提供 validateBeforeSave: true 选项给 insertMany(),但它只运行验证,不运行自定义 pre.save() 钩子。
- ⚠️
insertMany([...], { validateBeforeSave: true })→ 触发验证,但不触发pre.save() - ✅ 真正需要中间件逻辑的批量场景,应循环构造 Document 实例再逐个
save(),或手动在循环中调用你的预处理逻辑 - ?
bulkWrite()完全绕过所有中间件和验证,纯数据库层操作
你有没有在中间件里忘了 return 或 await?
异步中间件必须明确告知 Mongoose “我还没完”。如果用了 async 函数但没 return Promise,或者用了回调风格但忘了调用 next(),save() 就会挂起或静默失败。
- ❌ 错误:
schema.pre('save', async function() { await doAsync(); });—— 没return,Mongoose 不知道要等这个 Promise - ✅ 正确:
schema.pre('save', async function() { await doAsync(); });(Mongoose v5.9+ 支持隐式等待) - ✅ 更稳妥:
schema.pre('save', function(next) { doAsync().then(() => next()).catch(next); }); - ⚠️ 注意:如果你在中间件里修改了
this字段,但没设this.markModified('field'),某些类型(如 nested object、array)的变更可能不会被持久化
真正容易被忽略的是:中间件只属于 Document 生命周期,而 Model 方法(update、findAndModify 等)天然游离在这个生命周期之外。一旦你选择跳过 Document 实例,就等于主动放弃了 Mongoose 最核心的抽象能力 —— 验证、默认值、钩子、类型转换,这些都不会自动发生。











