Mongoose查询含重复ID的数组时返回对应重复文档的方法
解决Mongoose按重复ID数组返回对应重复文档的问题
这个问题确实很常见——$in查询会自动去重返回唯一文档,但我们需要保留原数组的重复顺序和次数。这里有两种实用的解决方案,根据你的场景选就行:
方法一:后端映射(简单易实现,推荐中小数据量)
核心思路是先获取所有唯一的歌曲文档,再把它们转成一个以_id为键的映射对象,最后遍历原ID数组,逐个取出对应的歌曲文档。这样就能完美还原原数组的重复次数和顺序了。
代码示例:
// 假设用户的收听记录ID数组是 user.listenHistoryIds = ['a', 'a', 'a', 'b', 'c'] const uniqueSongIds = [...new Set(user.listenHistoryIds)]; // 先查询所有唯一的歌曲 const uniqueSongs = await Song.find({ _id: { $in: uniqueSongIds } }).exec(); // 将歌曲转成键值对映射,方便快速查找(注意把ObjectId转成字符串匹配) const songMap = uniqueSongs.reduce((map, song) => { map[song._id.toString()] = song; return map; }, {}); // 按照原ID数组的顺序和重复次数生成最终列表 const userPlayHistory = user.listenHistoryIds.map(id => songMap[id.toString()]);
额外优化:处理无效ID
如果原数组里有不存在的歌曲ID,上面的map会返回undefined,可以加个过滤步骤清理掉:
const userPlayHistory = user.listenHistoryIds .map(id => songMap[id.toString()]) .filter(song => song); // 移除不存在的歌曲项
这种方法的优点是代码简洁易懂,性能也不错——因为只查一次数据库,剩下的都是内存操作,适合大多数用户收听记录的场景。
方法二:MongoDB聚合管道(数据库层处理,适合大数据量)
如果你的用户收听记录特别长,或者希望所有逻辑都在数据库层面完成,可以用MongoDB的聚合框架来实现。核心是通过$map和$arrayToObject来构建映射,再还原原数组的结构:
代码示例:
const userPlayHistory = await Song.aggregate([ // 第一步:获取所有唯一的歌曲文档 { $match: { _id: { $in: [...new Set(user.listenHistoryIds)] } } }, // 第二步:将歌曲数组转成键值对映射(ObjectId转字符串作为键) { $group: { _id: null, songMap: { $push: { k: { $toString: "$_id" }, v: "$$ROOT" } } } }, // 第三步:根据原ID数组,映射出包含重复项的歌曲列表 { $project: { playHistory: { $map: { input: user.listenHistoryIds, as: "id", in: { $arrayElemAt: [ "$songMap.v", { $indexOfArray: ["$songMap.k", "$$id"] } ] } } } } }, // 第四步:展开数组并返回最终文档 { $unwind: "$playHistory" }, { $replaceRoot: { newRoot: "$playHistory" } } ]).exec();
这个方法把所有逻辑都交给数据库处理,后端只需要调用聚合查询即可,适合数据量较大、不想占用后端内存的场景,但代码相对复杂一些。
关键注意点
- 一定要统一
_id的类型:MongoDB的_id是ObjectId类型,而用户数组里的ID通常是字符串,所以必须转成字符串后再做映射匹配,否则会出现找不到的情况。 - 两种方法都能保留原数组的顺序——因为
map操作是严格按照原数组的顺序执行的。
内容的提问来源于stack exchange,提问作者Yooooomi
相关产品推荐
相关产品推荐

