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
相关产品推荐
相关产品推荐

