
本文介绍如何在 express + mongodb 应用中,通过一次请求返回所有专辑数据,并为每个专辑动态添加其关联歌曲数量,避免因异步处理不当导致空响应或性能瓶颈。
本文介绍如何在 express + mongodb 应用中,通过一次请求返回所有专辑数据,并为每个专辑动态添加其关联歌曲数量,避免因异步处理不当导致空响应或性能瓶颈。
在构建音乐播放器后端时,常需提供 /explore/albums 这类聚合接口:既要返回专辑基础信息(如名称、封面、发行年份等),又要附带该专辑包含的歌曲总数。由于 MongoDB 中专辑(Albums)与歌曲(Songs)通常分集合存储,且专辑文档本身不冗余保存 numberOfSongs 字段,因此必须在查询时动态统计。
原始代码存在两个关键问题:
- 使用 .map(async () => {}) 但未 await 其返回的 Promise 数组,导致 albums 变成一个充满 pending Promise 的数组,而非实际数据;
- 错误调用 .length 在未等待 _Songs.find(...) 完成前执行,结果恒为 undefined 或 0;
- 缺少错误状态码返回,异常时仅打印日志,前端无法获知服务失败。
✅ 正确做法是:先获取全部专辑,再并行统计每张专辑的歌曲数,最后合并响应。推荐使用 Promise.all() 实现并发查询,兼顾可读性与性能:
Router.route("/explore/albums")
.get(async (req, res) => {
try {
// 1. 获取所有专辑(同步返回数组)
const albums = await _Albums.find().lean(); // .lean() 提升性能,返回纯 JS 对象
// 2. 并行统计每张专辑的歌曲数量(推荐 countDocuments 而非 find().length)
const albumsWithSongs = await Promise.all(
albums.map(async (album) => {
const numberOfSongs = await _Songs.countDocuments({ album: album.name });
return { ...album, numberOfSongs }; // 直接展开合并(lean 后无需 toObject)
})
);
// 3. 统一返回结构化 JSON
res.json(albumsWithSongs);
} catch (e) {
console.error("[GET /explore/albums] Error:", e);
res.status(500).json({
error: "Internal Server Error",
message: "Failed to fetch albums with song counts"
});
}
});
? 关键优化说明:
- countDocuments() 比 find().length 更高效:它只返回计数值,不加载完整文档,减少内存与网络开销;
- .lean() 使 Mongoose 返回普通 JavaScript 对象(而非 Mongoose Document 实例),避免后续 toObject() 调用,提升序列化速度;
- Promise.all() 确保所有歌曲计数并发执行,总耗时 ≈ 最慢单次查询时间,而非累加;
- 显式 res.status(500) 和结构化错误响应,符合 REST API 最佳实践,便于前端统一错误处理。
⚠️ 注意事项:
- 若专辑名存在空格、大小写不一致或特殊字符,请确保 Songs 集合中 album 字段与 Albums.name 严格匹配(建议在数据写入时标准化,或使用 albumId ObjectId 关联替代字符串匹配);
- 高频访问时可考虑添加缓存(如 Redis),对 album → songCount 映射做 TTL 缓存;
- 若专辑量极大(如 >1000),应分页或改用聚合管道($lookup + $group)在数据库层完成关联统计,避免应用层压力。
通过以上实现,你将获得一个健壮、高效且易于维护的专辑聚合接口,每个返回对象都包含原始专辑字段与准确的 numberOfSongs,为前端音乐探索页提供坚实的数据支撑。











