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

Mongoose如何更新MongoDB嵌套元素 实现物品数量增减与不存在则新增

Mongoose 角色物品增减操作实现

所有更新优先使用MongoDB原子操作实现,不要采用「先查询整文档→内存修改数组→save保存」的流程,避免并发场景下出现覆盖写入导致数量异常。

一、物品数量增加(不存在则新建条目)

如果使用MongoDB 4.2及以上版本(绝大多数云厂商托管MongoDB、自建新版本都支持),可以用单条聚合管道更新原子完成逻辑,无需多次请求:

/**
 * 增加指定角色的物品数量
 * @param {ObjectId} characterId 角色ID
 * @param {string} itemName 物品名称
 * @param {number} itemCategory 物品分类
 * @param {number} addNum 要增加的数量 必须为正整数
 */
const addItemQuantity = async (characterId, itemName, itemCategory, addNum) => {
  return Character.updateOne(
    { _id: characterId },
    [
      {
        $set: {
          items: {
            $cond: [
              // 判断是否已存在同名同分类的物品
              {
                $gt: [
                  {
                    $size: {
                      $filter: {
                        input: "$items",
                        cond: {
                          $and: [
                            { $eq: ["$$this.name", itemName] },
                            { $eq: ["$$this.category", itemCategory] }
                          ]
                        }
                      }
                    }
                  },
                  0
                ]
              },
              // 已存在:遍历数组给对应物品累加数量
              {
                $map: {
                  input: "$items",
                  as: "item",
                  in: {
                    $cond: [
                      {
                        $and: [
                          { $eq: ["$$item.name", itemName] },
                          { $eq: ["$$item.category", itemCategory] }
                        ]
                      },
                      { $mergeObjects: ["$$item", { quantity: { $add: ["$$item.quantity", addNum] } }] },
                      "$$item"
                    ]
                  }
                }
              },
              // 不存在:追加新物品条目到数组
              { $concatArrays: ["$items", [{ name: itemName, category: itemCategory, quantity: addNum }]] }
            ]
          }
        }
      }
    ]
  );
};

如果MongoDB版本低于4.2不支持聚合更新,用两段式更新即可,记得给集合加{_id:1, "items.name":1, "items.category":1}的索引提升匹配效率:

const addItemQuantity = async (characterId, itemName, itemCategory, addNum) => {
  // 先尝试给已有物品加数量
  const updateRes = await Character.updateOne(
    {
      _id: characterId,
      "items.name": itemName,
      "items.category": itemCategory
    },
    { $inc: { "items.$.quantity": addNum } }
  );
  // 没匹配到对应物品则新建条目
  if (updateRes.matchedCount === 0) {
    await Character.updateOne(
      { _id: characterId },
      { $push: { items: { name: itemName, category: itemCategory, quantity: addNum } } }
    );
  }
};

二、物品数量扣减

扣减逻辑需要注意边界:禁止数量扣为负数,可按需选择是否自动删除数量为0的物品条目。

基础扣减(保留0数量条目)

/**
 * 扣减指定角色的物品数量
 * @param {ObjectId} characterId 角色ID
 * @param {string} itemName 物品名称
 * @param {number} itemCategory 物品分类
 * @param {number} deductNum 要扣减的数量 必须为正整数
 */
const deductItemQuantity = async (characterId, itemName, itemCategory, deductNum) => {
  return Character.updateOne(
    {
      _id: characterId,
      "items.name": itemName,
      "items.category": itemCategory
    },
    [
      {
        $set: {
          items: {
            $map: {
              input: "$items",
              as: "item",
              in: {
                $cond: [
                  {
                    $and: [
                      { $eq: ["$$item.name", itemName] },
                      { $eq: ["$$item.category", itemCategory] }
                    ]
                  },
                  // 扣减后数量最小为0,不会出现负数
                  { $mergeObjects: ["$$item", { quantity: { $max: [0, { $subtract: ["$$item.quantity", deductNum] }] } }] },
                  "$$item"
                ]
              }
            }
          }
        }
      }
    ]
  );
};

扣减后自动清理0数量条目

如果业务上不需要保留数量为0的物品,在扣减逻辑后追加过滤步骤即可:

const deductItemAndClean = async (characterId, itemName, itemCategory, deductNum) => {
  return Character.updateOne(
    { _id: characterId },
    [
      {
        $set: {
          items: {
            $let: {
              vars: {
                // 先执行扣减逻辑
                deductedItems: {
                  $map: {
                    input: "$items",
                    as: "item",
                    in: {
                      $cond: [
                        {
                          $and: [
                            { $eq: ["$$item.name", itemName] },
                            { $eq: ["$$item.category", itemCategory] }
                          ]
                        },
                        { $mergeObjects: ["$$item", { quantity: { $max: [0, { $subtract: ["$$item.quantity", deductNum] }] } }] },
                        "$$item"
                      ]
                    }
                  }
                }
              },
              in: {
                // 过滤掉所有数量为0的条目
                $filter: {
                  input: "$$deductedItems",
                  cond: { $gt: ["$$this.quantity", 0] }
                }
              }
            }
          }
        }
      }
    ]
  );
};

常见坑点说明

  • 匹配物品时必须同时校验name和category两个字段,避免同名不同分类的物品被错误修改
  • 不要在Mongoose里对items数组配置unique约束,嵌套数组的唯一约束在MongoDB中不会按预期生效
  • 所有数量计算都放在数据库端完成,不要在Node.js层读取文档后计算再写回,并发下必然出现数量覆盖问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 22:18:14