如何在Mongoose中为文档插入触发函数?解决upsert失效问题
需求与尝试
需求:每次在集合中插入/创建元素时触发函数(更新时不触发),且需获取新创建元素的ID。
我尝试通过Mongoose的save钩子实现:
schema.pre("save", function() { this.$locals.wasNew = this.isNew; }); schema.post("save", function() { if (this.$locals.wasNew) { console.log(`User ${this.id} created!`); } });
这段代码在调用UserModel.create()时完全正常:
await UserModel.create({ mail });
但当我调用带upsert: true的findOneAndUpdate()时,钩子完全没触发预期的逻辑:
const user = await UserModel.findOneAndUpdate( { mail }, { $set: { mail } }, { new: true, upsert: true } );
疑问:upsert操作似乎没触发isNew为true的save钩子,是我操作错了吗?有没有其他靠谱的实现方式?
原因与解决方案
为什么save钩子不生效?
findOneAndUpdate(包括带upsert的场景)是MongoDB的原子操作,默认不会走Mongoose的文档实例化流程——也就是说它直接在数据库层面执行更新/插入,不会创建Mongoose文档对象,自然也就不会触发save钩子,更不会设置isNew属性。
替代方案
方案1:监听findOneAndUpdate的post钩子(推荐)
直接监听findOneAndUpdate的后置钩子,通过MongoDB返回的原始结果判断是否是新创建的文档:
schema.post('findOneAndUpdate', function(doc, res) { // rawResult里的upserted字段存在,说明是upsert创建的新文档 if (res.rawResult?.upserted) { const newUserId = res.rawResult.upserted[0]._id; console.log(`User ${newUserId} created!`); } });
注意:调用findOneAndUpdate时必须加上rawResult: true参数,才能拿到包含upsert信息的原始结果:
const user = await UserModel.findOneAndUpdate( { mail }, { $set: { mail } }, { new: true, upsert: true, rawResult: true } );
方案2:强制触发save钩子(不推荐)
如果非要复用save钩子的逻辑,可以在调用findOneAndUpdate时添加runValidators: true和context: 'query'参数,强制让Mongoose实例化文档并触发save钩子。但这种方式会破坏原子性,性能不如原生钩子,只适合特殊场景:
const user = await UserModel.findOneAndUpdate( { mail }, { $set: { mail } }, { new: true, upsert: true, runValidators: true, context: 'query' } );
方案3:统一使用create/save方法
如果业务逻辑允许,尽量统一用Model.create()或者先查询文档,不存在时再创建并调用doc.save(),这样就能正常触发save钩子中的isNew判断,逻辑更统一。
内容的提问来源于stack exchange,提问作者Herobrine

