聚合查询为何按_id排序?如何维持输入数组的ID顺序?
问题
通过一组_id数组检索文档时,返回结果总是按_id字母序排序,即使在调用时传入sort: undefined,也无法维持输入数组的原始顺序。
相关代码如下:
聚合查询函数
export async function getApprovedPopulatedResources(match?: mongoose.AnyObject, sort?: Record<string, 1 | -1>, page?: number, pageSize?: number) { let resourcesFilter = []; if (sort) resourcesFilter.push({ $sort: sort }) if (page && pageSize) { resourcesFilter.push({ $skip: (page - 1) * pageSize }); resourcesFilter.push({ $limit: pageSize }); } let aggregationResult = await Resource.aggregate() .match({ approved: true, ...match }) .lookup({ from: "resourcevotes", localField: "_id", foreignField: "resourceId", as: "votes", }) .facet({ resources: resourcesFilter, totalCount: [{ $group: { _id: null, count: { $sum: 1 } } }] }).exec(); const resourcesPopulated = await Resource.populate(aggregationResult[0].resources, [ { path: "submittingUser" }, { path: "lastUpdatingUser" }, ] ); const totalCount = aggregationResult[0].totalCount[0]?.count || 0; const result = { resources: resourcesPopulated, totalCount: totalCount, pageCount: Math.ceil(totalCount / (pageSize || totalCount)), }; return result; }
调用代码
const resourceIds = [...] // 这里的_id数组是正确的顺序 const result = await getApprovedPopulatedResources( // 返回的resources顺序错误,按_id字母序排列 { _id: { $in: resourceIds } }, undefined, page, pageSize );
解决方案
MongoDB的$in操作符不会保留输入数组的顺序,默认会按照文档的存储顺序或索引顺序返回结果(此处表现为_id的字母序)。要维持输入_id数组的原始顺序,需要在聚合管道中添加基于输入数组位置的排序逻辑:
修改后的聚合函数
export async function getApprovedPopulatedResources(match?: mongoose.AnyObject, sort?: Record<string, 1 | -1>, page?: number, pageSize?: number) { let resourcesFilter = []; const idInArray = match?._id?.$in; // 如果传入了_id的$in数组且未指定自定义排序,添加基于索引的排序规则 if (idInArray && Array.isArray(idInArray) && !sort) { resourcesFilter.unshift({ $sort: { order: 1 } }); } // 初始化聚合管道 let aggregationPipeline = Resource.aggregate() .match({ approved: true, ...match }); // 给匹配的文档添加order字段,值为该文档_id在输入数组中的索引 if (idInArray && Array.isArray(idInArray) && !sort) { aggregationPipeline = aggregationPipeline.addFields({ order: { $indexOfArray: [idInArray, "$_id"] } }); } // 继续后续聚合步骤 aggregationPipeline = aggregationPipeline .lookup({ from: "resourcevotes", localField: "_id", foreignField: "resourceId", as: "votes", }) .facet({ resources: resourcesFilter, totalCount: [{ $group: { _id: null, count: { $sum: 1 } } }] }); const aggregationResult = await aggregationPipeline.exec(); const resourcesPopulated = await Resource.populate(aggregationResult[0].resources, [ { path: "submittingUser" }, { path: "lastUpdatingUser" }, ] ); const totalCount = aggregationResult[0].totalCount[0]?.count || 0; return { resources: resourcesPopulated, totalCount, pageCount: Math.ceil(totalCount / (pageSize || totalCount)), }; }
原理说明
$indexOfArray操作符会返回当前文档的_id在输入resourceIds数组中的位置索引(第一个元素索引为0,第二个为1,以此类推)。- 通过按
order字段升序排序,就能让结果严格遵循输入数组的顺序返回。
注意事项
- 如果不需要返回
order字段,可以在facet之前添加$project步骤移除该字段,例如:.project({ order: 0 })。 - 当调用时传入自定义
sort参数时,会优先使用自定义排序规则,不会触发上述逻辑,保证函数的通用性。
内容的提问来源于stack exchange,提问作者Florian Walther
相关产品推荐
相关产品推荐

