使用带负值的$inc操作符时Mongoose Schema setter未触发问题
问题原因与解决方案
核心原因
Mongoose的字段set函数是文档实例级别的钩子,只有当你通过文档实例(比如user.money.balance = xxx后调用user.save())修改字段时才会触发。而findByIdAndUpdate这类直接操作数据库的更新方法,会绕过实例的setter逻辑,直接将$inc这类原生MongoDB更新指令发送到数据库执行——不管正负值,setter都不会被触发(你遇到的“有时不触发”大概率是测试时误操作使用了实例修改的场景)。
解决方案
1. 改用实例修改+save的方式(最稳妥)
这种方式会完整走Mongoose的实例生命周期,自动触发setter、验证器等逻辑:
const user = await User.findById(userId); user.money.balance += -1.3245479576; await user.save();
2. 用预更新钩子处理格式化(适合批量更新场景)
如果需要保留findByIdAndUpdate的性能优势,可以在Schema上添加预更新钩子,手动计算并格式化值:
userSchema.pre('findOneAndUpdate', async function(next) { const update = this.getUpdate(); // 检测是否有针对money.balance的$inc操作 if (update.$inc && typeof update.$inc['money.balance'] === 'number') { // 获取当前文档的最新值 const doc = await this.model.findOne(this.getQuery()); if (!doc) return next(); // 计算累加后的值并复用Schema定义的setter const newValue = doc.money.balance + update.$inc['money.balance']; const formattedValue = userSchema.path('money.balance').options.set(newValue); // 替换$inc为直接赋值,避免数据库端重复累加 delete update.$inc; update['money.balance'] = formattedValue; } next(); });
⚠️ 注意:这种方式存在竞态条件——如果在查询文档和更新之间有其他请求修改了该字段,会导致数据不准确。高并发场景建议优先用第一种方案。
3. 修正setter的返回类型(优化现有逻辑)
你当前的setter用toFixed(4)返回字符串,但字段类型定义为Number,Mongoose自动转换可能出现意外问题。建议改为返回数字类型的格式化逻辑:
set: (v) => { const num = Number(v); return isNaN(num) ? 0 : Math.round(num * 10000) / 10000; },
这样确保存入数据库的是标准数字类型,且稳定保留四位小数。
内容的提问来源于stack exchange,提问作者Nadir Z
相关产品推荐
相关产品推荐

