MongoDB中如何仅更新数组过滤器匹配的第一个元素
问题描述
我需要仅更新spotsAvailable数组中userId为"empty"的一个子文档。之前的查询会更新所有userId为"empty"的匹配子文档,不符合需求(如下示例)。我试过用聚合并添加limit阶段来仅更新一个元素,但聚合无法修改原始文档。因此只能选择用数组过滤器来仅更新第一个匹配项,这是否可行?我觉得应该有办法只更新数组过滤器中的第一个匹配项,如果没有的话,这绝对是个值得添加的功能。
我的代码
此代码会更新所有userId为"empty"的对象
const client = await clientPromise; const db = client.db(); // const query = db.collection('events').aggregate(agg); const query = await db.collection('events').updateOne({ _id: new ObjectId("6398c34ca67dbe3286452f23"), createdBy: new ObjectId("636c1778f1d09191074f9690"), "weights.weight": 12 }, { $set: { "weights.$.spotsAvailable.$[el2]": { "name": "Wayne Wrestler", "userId": new ObjectId("636c1778f1d09191074f9690") } } }, { arrayFilters: [{ "el2": { "userId": "empty" } }] })
示例文档
Event:
{ "_id": { "$oid": "6398c34ca67dbe3286452f23" }, "name": "test", "createdBy": { "$oid": "636c1778f1d09191074f9690" }, "description": "testing", "date": { "$date": { "$numberLong": "1645488000000" } }, "location": { "type": "Point", "coordinates": [ 0, 0 ] }, "weights": [ { "spotsAvailable": [ { "name": "empty", "userId": "empty" }, { "name": "empty", "userId": "empty" }, { "name": "empty", "userId": "empty" } ], "weight": 12 }, { "spotsAvailable": [ { // 本应仅更新其中一个,但两个都被更新了 "name": "Wayne Wrestler", "userId": { "$oid": "636c1778f1d09191074f9690" } }, { "name": "Wayne Wrestler", "userId": { "$oid": "636c1778f1d09191074f9690" } } ], "weight": 15 } ], "eventApplicants": [ { "userId": { "$oid": "636c1778f1d09191074f9690" }, "name": "Wayne Wrestler", "weight": 12 } ] }
User:
{ "_id": { "$oid": "636c1778f1d09191074f9690" }, "name": "Wayne Wrestler", "email": "wakywayne80@gmail.com", "image": "https://lh3.googleusercontent.com/a/ALm5wu32gXjDIRxncjjQA9I4Yl-sjFH5EWsTlmvdM_0kiw=s96-c", "emailVerified": { "$date": { "$numberLong": "1670864727212" } }, "createdEvents": [ { "createdEventName": "test", "createdEventDate": { "$date": { "$numberLong": "1645488000000" } }, "createdEventDescription": "testing", "createdEventWeights": [ { "weight": "12", "filled": [ false, false, false ] }, { "weight": "15", "filled": [ false, false ] } ], "createdEventId": { "$oid": "6398c34ca67dbe3286452f23" } } ], "userSignedUpEvents": [], "availableWeights": [ 1, 123 ], "signedUpEvents": [ { "eventId": { "$oid": "636c722f67642c30dc5ffc30" }, "eventName": "Utah", "eventDate": { "$date": { "$numberLong": "1667913330000" } }, "accepted": false }, { "eventId": { "$oid": "636c722f67642c30dc5ffc30" }, "eventName": "Utah", "eventDate": { "$date": { "$numberLong": "1667913330000" } }, "accepted": false }, { "eventId": { "$oid": "637ec484ac2d675b30590b47" }, "eventName": "Maybe?", "eventDate": { "$date": { "$numberLong": "1672272000000" } }, "accepted": false }, { "eventId": { "$oid": "636c722f67642c30dc5ffc30" }, "eventName": "Utah", "eventDate": { "$date": { "$numberLong": "1667913330000" } }, "accepted": false }, { "eventId": { "$oid": "638d5274628db2a7bf61df49" }, "eventName": "Eva's", "eventDate": { "$date": { "$numberLong": "1698019200000" } }, "accepted": false }, { "eventId": { "$oid": "636c722f67642c30dc5ffc30" }, "eventName": "Utah", "eventDate": { "$date": { "$numberLong": "1667913330000" } }, "accepted": false }, { "eventId": { "$oid": "6398a922abb5c168ede595fb" }, "eventName": "Nikko's event", "eventDate": { "$date": { "$numberLong": "1670976000000" } }, "accepted": false }, { "eventId": { "$oid": "6398a922abb5c168ede595fb" }, "eventName": "Nikko's event", "eventDate": { "$date": { "$numberLong": "1670976000000" } }, "accepted": false }, { "eventId": { "$oid": "6398c34ca67dbe3286452f23" }, "eventName": "test", "eventDate": { "$date": { "$numberLong": "1645488000000" } }, "accepted": false }, { "eventId": { "$oid": "6398c34ca67dbe3286452f23" }, "eventName": "test", "eventDate": { "$date": { "$numberLong": "1645488000000" } }, "accepted": false }, { "eventId": { "$oid": "6398c34ca67dbe3286452f23" }, "eventName": "test", "eventDate": { "$date": { "$numberLong": "1645488000000" } }, "accepted": false }, { "eventId": { "$oid": "6398c34ca67dbe3286452f23" }, "eventName": "test", "eventDate": { "$date": { "$numberLong": "1645488000000" } }, "accepted": false } ] }
已尝试的方法
- 不使用
new ObjectId语法传入变量 - 使用
new ObjectId语法传入变量 - 使用从Compass复制到Node驱动的聚合代码中的硬编码值
以上方法要么无效,要么会导致所有userId为"empty"的子文档被更新
解决方案
直接使用数组过滤器无法实现仅更新第一个匹配的子文档,不过可以通过以下两种方式达成需求:
方法一:聚合管道单步更新(MongoDB 4.2+支持)
利用聚合管道的表达式精准定位第一个空元素,一次性完成更新:
const query = await db.collection('events').updateOne( { _id: new ObjectId("6398c34ca67dbe3286452f23"), createdBy: new ObjectId("636c1778f1d09191074f9690"), "weights.weight": 12 }, [ { $set: { weights: { $map: { input: "$weights", as: "w", in: { $cond: [ { $eq: ["$$w.weight", 12] }, { ..."$$w", spotsAvailable: { $map: { input: "$$w.spotsAvailable", as: "s", in: { $cond: [ { $and: [ { $eq: ["$$s.userId", "empty"] }, { $eq: [{ $indexOfArray: ["$$w.spotsAvailable.userId", "empty"] }, { $indexOfArray: ["$$w.spotsAvailable", "$$s"] }] } ]}, { "name": "Wayne Wrestler", "userId": new ObjectId("636c1778f1d09191074f9690") }, "$$s" ] } } } }, "$$w" ] } } } } } ] )
通过$indexOfArray获取第一个userId为"empty"的元素索引,仅更新该索引对应的子文档。
方法二:先查索引再更新
分两步操作,先获取第一个空元素的索引,再通过索引直接定位更新:
// 第一步:查询获取第一个empty元素的索引 const event = await db.collection('events').findOne( { _id: new ObjectId("6398c34ca67dbe3286452f23"), createdBy: new ObjectId("636c1778f1d09191074f9690"), "weights.weight": 12 }, { projection: { "weights.$.spotsAvailable": 1 } } ); const firstEmptyIndex = event.weights[0].spotsAvailable.findIndex(s => s.userId === "empty"); if (firstEmptyIndex !== -1) { // 第二步:通过索引精准更新 const updateResult = await db.collection('events').updateOne( { _id: new ObjectId("6398c34ca67dbe3286452f23"), createdBy: new ObjectId("636c1778f1d09191074f9690"), "weights.weight": 12 }, { $set: { [`weights.$.spotsAvailable.${firstEmptyIndex}`]: { "name": "Wayne Wrestler", "userId": new ObjectId("636c1778f1d09191074f9690") } } } ); }
这种方式逻辑直观,适合对聚合管道不熟悉的场景。
内容的提问来源于stack exchange,提问作者Wayne
相关产品推荐
相关产品推荐

