Mongoose单查询更新数组元素及其他属性的实现疑问
问题
需要创建MongoDB聚合查询,实现返回结果的同时更新文档的name属性和map数组:
- 匹配
prefix的数组项:仅更新指定字段(比如示例中的name),保留原项其他属性 - 新增
prefix不存在的数组项 - 保留原数组中未被匹配的项
现有文档
{ "_id": ObjectId("65141e3d8fc74027df89e79d"), "version": "14c613f7-84f9-4df6-a9c2-8c8923b40c31", "name": "decoder-facd32602f9faa54", "map": [ { "prefix": 101, "name": "Digital Input", "scale": 1, "size": 1, "rage": [0, 255], "value_type": "uint8" }, { "prefix": 102, "name": "Digital Output", "scale": 1, "size": 1, "rage": [0, 255], "value_type": "uint8" } ] }
接收的更新数据
{ name: "decoder-rename-update", map: [ { prefix: 101, name: "Digital Input UPDATE", scale: 1, size: 1, rage: [0, 255], value_type: "uint8" }, { prefix: 103, name: "Analog Output", scale: 1, size: 1, rage: [0, 255], value_type: "uint8" } ] }
预期更新结果
{ "_id": ObjectId("65141e3d8fc74027df89e79d"), "version": "14c613f7-84f9-4df6-a9c2-8c8923b40c31", "name": "decoder-rename-update", "map": [ { "prefix": 101, "name": "Digital Input", "scale": 1, "size": 1, "rage": [0, 255], "value_type": "uint8" }, { "prefix": 102, "name": "Digital Output UPDATE", "scale": 1, "size": 1, "rage": [0, 255], "value_type": "uint8" }, { "prefix": 103, "name": "Analog Output", "scale": 1, "size": 1, "rage": [0, 255], "value_type": "uint8" } ] }
尝试的聚合代码及错误
尝试的代码:
const decoder = await this.decoderModel.aggregate( [ { $match: { _id } }, { $set: { name } }, { $project: { map: { $concatArrays: [ { $map: { input: '$map', as: 'map', in: { cond: [ { $in: [ "$$map.prefix", map.map( d => d.prefix ) ] }, { $mergeObjects: [ "$$map", { $arrayElemAt: [ { $filter: { input: map, cond: { $eq: [ `$$this.prefix`, `$$map.prefix` ] } } }, 0 ] } ] }, `$$map` ] } } }, { $filter: { input: map, cond: { $not: { $in: [ `$$this.prefix`, `$$map.prefix` ] } } } } ] } } } ] );
出现的错误:
MongoServerError: Invalid $project :: caused by :: FieldPath field names may not start with '$'. Consider using $getField or $setField. at Connection.onMessage (E:\Gitlab\babylon-data-registery\node_modules\mongodb\src\cmap\connection.ts:413:18) at MessageStream.<anonymous> (E:\Gitlab\babylon-data-registery\node_modules\mongodb\src\cmap\connection.ts:243:56) at MessageStream.emit (node:events:513:28) at processIncomingData (E:\Gitlab\babylon-data-registery\node_modules\mongodb\src\cmap\message_stream.ts:193:12) at MessageStream._write (E:\Gitlab\babylon-data-registery\node_modules\mongodb\src\cmap\message_stream.ts:74:5) at writeOrBuffer (node:internal/streams/writable:392:12) at _write (node:internal/streams/writable:333:10) at MessageStream.Writable.write (node:internal/streams/writable:337:10) at TLSSocket.ondata (node:internal/streams/readable:766:22) at TLSSocket.emit (node:events:513:28) at addChunk (node:internal/streams/readable:324:12) at readableAddChunk (node:internal/streams/readable:297:9) at TLSSocket.Readable.push (node:internal/streams/readable:234:10) at TLSWrap.onStreamRead (node:internal/stream_base_commons:190:23) Waiting for the debugger to disconnect...
解决方案
错误原因
- 直接在聚合管道中使用客户端变量
map,MongoDB会将其解析为字段路径,而非传入的更新数据 - 自定义变量名
as: 'map'与聚合操作符$map重名,导致变量引用混淆 - 错误的字段路径引用格式,引发字段名不能以
$开头的报错
修正后的聚合代码
const decoder = await this.decoderModel.aggregate([ { $match: { _id } }, // 先将客户端传入的更新map存入临时字段,避免与原文档map混淆 { $set: { name, updateMap: map } }, { $project: { version: 1, name: 1, map: { $concatArrays: [ // 处理原数组中匹配prefix的项:合并更新数据,保留原项未被更新的属性 { $map: { input: '$map', as: 'existingItem', in: { $mergeObjects: [ '$$existingItem', // 从更新map中匹配对应prefix的项,只取需要更新的字段 { $arrayElemAt: [ { $filter: { input: '$updateMap', cond: { $eq: ['$$this.prefix', '$$existingItem.prefix'] } } }, 0 ] } ] } } }, // 添加更新map中不存在于原数组的新项 { $filter: { input: '$updateMap', cond: { $not: { $in: ['$$this.prefix', '$map.prefix'] } } } } ] } } }, // 移除临时字段updateMap { $unset: 'updateMap' } ]);
代码说明
- $set阶段:将客户端传入的
name和map(重命名为updateMap)存入临时字段,确保聚合管道能正确访问更新数据 - $project阶段:
- 第一个
$map:遍历原文档的map数组,对每个项,用$mergeObjects合并原项和更新数据中匹配prefix的项,保留原项未被更新的属性 - 第二个
$filter:筛选出updateMap中prefix不存在于原map的项,作为新项添加 - 用
$concatArrays合并处理后的原数组项和新项,得到最终的map数组
- 第一个
- $unset阶段:移除临时字段
updateMap,返回干净的结果
额外说明
如果需要同时更新数据库中的文档,而非仅返回处理后的结果,可以将聚合管道与$merge阶段结合,将结果写回原集合:
await this.decoderModel.aggregate([ // 上述所有聚合阶段... { $merge: { into: 'decoders', // 替换为你的集合名 on: '_id', whenMatched: 'replace', whenNotMatched: 'discard' } } ]);
内容的提问来源于stack exchange,提问作者Andy95
相关产品推荐
相关产品推荐

