使用Mongoose.findByIdAndUpdate时pre-update钩子引发错误求助
嘿,我太懂你这种头疼的情况了——既要保留忘记密码功能依赖的钩子,又要让用户正常修改个人信息,还遇到了莫名其妙的undefined错误。咱们先把问题根源理清楚,再给你几个靠谱的解决方案:
为什么会出现这个错误?
你踩的是Mongoose钩子的一个常见坑:findByIdAndUpdate这类查询式更新方法,触发的pre('update')或pre('findOneAndUpdate')钩子中,this指向的是查询对象(Query),而不是你要更新的用户文档(Document)。
你之前写的钩子里肯定在尝试访问this.password,但查询对象里根本没有这个属性,自然就抛出了“Cannot read property 'password' of undefined”的错误。而你用save()方法修改密码(比如忘记密码功能)时,钩子的this是文档对象,所以能正常拿到password,这就是为什么只有修改个人信息时会报错。
三个可行的解决方案
方案1:区分更新场景,只在修改密码时执行钩子逻辑
这是最推荐的方案,既保留钩子功能,又不影响其他字段的更新。咱们可以在钩子里先判断这次更新有没有包含password字段,只有存在的时候才执行加密逻辑:
UserSchema.pre('findOneAndUpdate', function(next) { // 获取本次要更新的数据 const updateData = this.getUpdate(); // 只有当更新数据里包含password时,才执行加密操作 if (updateData.password) { // 这里替换成你实际的密码加密逻辑,比如用bcrypt bcrypt.hash(updateData.password, 10, (err, hashedPassword) => { if (err) return next(err); // 把明文密码替换成加密后的结果 this.setUpdate({ password: hashedPassword }); next(); }); } else { // 没改密码?直接跳过钩子逻辑就行 next(); } });
注意:建议用pre('findOneAndUpdate')代替pre('update'),因为findByIdAndUpdate本质上属于findOneAndUpdate的范畴,钩子触发更精准,不会被其他批量更新操作误触发。
方案2:改用「先查后更」的方式触发save钩子
如果你不想修改现有的pre('save')钩子(毕竟忘记密码功能已经依赖它了),可以放弃直接用findByIdAndUpdate,改成先查询到用户文档,修改后再调用save():
// 替换原来的findByIdAndUpdate调用 async function updateUserInfo(userId, updateData) { const user = await User.findById(userId); if (!user) throw new Error('用户不存在'); // 把要更新的字段赋值给用户文档 Object.assign(user, updateData); // 这里会触发pre('save')钩子,此时this是文档对象,能正常访问password return await user.save(); }
这个方法的缺点是多了一次数据库查询,但好处是完全复用你现有的钩子逻辑,不用改任何钩子代码。
方案3:检查钩子的函数定义方式
最后再确认一下:你的钩子是不是用普通function定义的?如果用了箭头函数,会绑定外层的this,导致无法获取到Mongoose的查询对象:
// ❌ 错误:箭头函数会导致this不是预期的Query对象 UserSchema.pre('findOneAndUpdate', () => { // 这里的this根本不是查询对象,肯定拿不到password }); // ✅ 正确:用普通function定义,this才会指向Query对象 UserSchema.pre('findOneAndUpdate', function(next) { // 这里的this是正常的查询对象,可以操作更新数据 });
总结
最省心的方案是方案1,既不用改业务代码,又能精准控制钩子的触发时机,完美兼顾忘记密码和个人信息修改的场景。
内容的提问来源于stack exchange,提问作者A.S.J

