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

Mongoose如何在执行更新前校验预览结果,异常时回滚文档?

MongoDB更新votes数组时保证percentage总和不超过100的实现方案

内置更新验证器确实无法直接对数组更新后的聚合值做规则校验,以下是三种可直接落地的方案,覆盖原子阻止非法写入、预览更新后回滚两类需求:


方案1:带条件的原子更新(优先推荐,无需回滚)

MongoDB 3.6及以上版本支持在更新查询条件中使用$expr聚合表达式,可以直接把「总和不超过100」作为更新执行的前置条件,不满足条件时根本不会匹配到文档,不会产生脏数据,完全不需要额外回滚逻辑,也没有并发安全问题。

新增数组元素示例

比如要新增一个占比2的投票项,写法如下:

db.ballots.updateOne(
  {
    _id: "目标文档ID",
    // 前置校验:现有总和 + 新增项占比 ≤100 才执行更新
    $expr: {
      $lte: [
        { $add: [ { $sum: "$votes.percentage" }, 2 ] },
        100
      ]
    }
  },
  {
    $push: {
      votes: { name: "Donald Duck", percentage: 2 }
    }
  }
)

如果执行返回的matchedCount为0,就说明总和超限,更新没有生效。

修改数组内已有元素示例

MongoDB 4.2及以上支持聚合管道更新,可以在更新逻辑中直接计算修改后的总和,超限则不写入:

db.ballots.updateOne(
  { _id: "目标文档ID" },
  [
    {
      $set: {
        // 先执行数组元素修改
        votes: {
          $map: {
            input: "$votes",
            in: {
              $cond: [
                { $eq: ["$$this.name", "Bugs Bunny"] },
                { name: "$$this.name", percentage: 51 }, // 修改为目标值
                "$$this"
              ]
            }
          }
        }
      }
    },
    {
      $set: {
        // 校验修改后的总和,超限则保持votes原值
        votes: {
          $cond: [
            { $lte: [ { $sum: "$votes.percentage" }, 100 ] },
            "$votes",
            "$votes"
          ]
        }
      }
    }
  ]
)

方案2:事务模式(支持预览更新内容,自动回滚)

如果你的Mongo是副本集/分片集群环境(支持多文档事务),可以用事务实现「先模拟计算预览,合法再提交,非法直接回滚」的逻辑,适合有复杂校验规则、需要提前返回更新后结果的场景,步骤如下:

  • 开启数据库会话和事务
  • 在事务内查询目标文档的当前数据
  • 在内存中模拟更新操作(修改数组项/新增项都可以),计算更新后的percentage总和
    • 如果总和超过100:直接抛出错误终止事务,原文档不会有任何改动,自动完成回滚
    • 如果总和符合要求:可以直接把内存中模拟的更新结果返回做预览,确认后再执行写入
  • 提交事务

Node.js驱动示例代码:

const session = await mongoClient.startSession()
try {
  let previewVotes = null
  await session.withTransaction(async () => {
    const targetDoc = await Ballot.findOne({ _id: "目标文档ID" }).session(session)
    // 模拟更新:比如修改Bugs Bunny的占比为51
    const updatedVotes = targetDoc.votes.map(item => {
      return item.name === "Bugs Bunny" ? { ...item, percentage: 51 } : item
    })
    const total = updatedVotes.reduce((sum, item) => sum + item.percentage, 0)
    if (total > 100) throw new Error("投票占比总和不能超过100")
    previewVotes = updatedVotes // 这里拿到的就是更新后的预览内容
    await Ballot.updateOne(
      { _id: "目标文档ID" },
      { $set: { votes: updatedVotes } }
    ).session(session)
  })
  // 事务提交后可以返回previewVotes给前端
  return previewVotes
} finally {
  await session.endSession()
}

这个方案逻辑完全可控,事务会自动处理并发冲突,不会出现脏写问题。


方案3:Mongoose钩子校验(适合Mongoose场景)

如果你用Mongoose作为ODM,可以通过Schema的预写入钩子做校验,在所有写入操作执行前拦截非法请求:

// 保存操作校验
ballotSchema.pre('save', function(next) {
  const total = this.votes.reduce((sum, item) => sum + item.percentage, 0)
  if (total > 100) return next(new Error("投票占比总和不能超过100"))
  next()
})

// 更新类操作校验
ballotSchema.pre(['updateOne', 'findOneAndUpdate', 'updateMany'], async function(next) {
  const currentDoc = await this.model.findOne(this.getQuery())
  const updateContent = this.getUpdate()
  // 根据updateContent里的$push/$set/$pull等操作,模拟计算更新后的votes数组
  // 计算总和后判断,超了就抛错终止写入
  next()
})

注意事项

不要用「先查文档、本地校验、再直接更新」的非原子写法,高并发场景下会出现两个请求同时查到总和为99,各写入占比2的项,最终总和变成103的问题。

内容的提问来源于stack exchange,提问作者lukas_o

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 05:09:25