You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用带负值的$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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 18:50:14