MongoDB中如何关联Likes与Posts集合查询用户对帖子的点赞状态
场景说明
我们的应用包含Posts集合和Likes集合,Likes集合存储关联的postId和用户ID,两个集合的Schema定义如下:
// Post Schema let postSchema = new Schema({ user: {type: Schema.Types.ObjectId, ref: "user"}, text: {type: String}, likeCount: {type: Number} }) // Like Schema let likeSchema = new Schema({ post: {type: Schema.Types.ObjectId, ref: "post"}, user: {type: Schema.Types.ObjectId, ref: "user"}, })
需求背景
分页拉取帖子列表时,需要同时返回当前登录用户是否已点赞该帖子的标识,执行查询时可获取到当前用户的userId。服务端使用Node.js开发,Likes集合会配置索引。
问题解答
1. 最优实现方案选择
分场景判断最优方案:
- 常规业务场景(单页帖子数量≤100,服务端MongoDB版本支持聚合):MongoDB聚合查询是最优解,仅需一次数据库查询即可完成数据拉取和点赞状态拼接,IO次数最少,性能最优。
- 特殊场景(聚合逻辑过于复杂、老项目代码规范要求避免大聚合、单页拉取帖子数量极大):二次查询+内存映射是最优解,代码可读性更高,维护成本更低,且两次查询均走索引,性能损失可忽略。
2. 聚合查询方案实现
聚合查询是非常合适的方案,只要给Likes集合配置post + user的联合索引,关联查询的效率极高,不会出现性能问题。
具体聚合管道代码如下(基于Mongoose):
const currentUserId = '当前登录用户的ObjectId'; const pageSize = 10; const pageNum = 1; const postList = await Post.aggregate([ // 分页阶段:跳过前面页码的记录 { $skip: (pageNum - 1) * pageSize }, // 分页阶段:取当前页的记录数 { $limit: pageSize }, // 关联Likes集合,匹配当前用户对该帖子的点赞记录 { $lookup: { from: 'likes', // Likes集合在MongoDB中的实际表名,默认是模型名小写加s let: { postId: '$_id' }, pipeline: [ { $match: { $expr: { $and: [ { $eq: ['$post', '$$postId'] }, { $eq: ['$user', mongoose.Types.ObjectId(currentUserId)] } ] } } } ], as: 'currentUserLikeRecords' } }, // 新增isLiked字段,判断是否存在点赞记录 { $addFields: { isLiked: { $gt: [{ $size: '$currentUserLikeRecords' }, 0] } } }, // 可选:删除多余的关联结果字段,精简返回值 { $project: { currentUserLikeRecords: 0 } } ])
索引优化建议:给LikeSchema配置联合唯一索引,既避免重复点赞,又加速关联查询:
likeSchema.index({ post: 1, user: 1 }, { unique: true });
3. 非聚合查询方案实现
不使用聚合的情况下,最优方案是「两次索引查询+内存映射」,具体实现如下:
const currentUserId = '当前登录用户的ObjectId'; const pageSize = 10; const pageNum = 1; // 第一步:分页查询帖子列表 const postList = await Post.find() .skip((pageNum - 1) * pageSize) .limit(pageSize) .lean(); // 转换为普通JS对象,方便追加自定义字段 // 提取当前页所有帖子的ID数组 const postIdList = postList.map(post => post._id); // 第二步:查询当前用户给这些帖子的点赞记录,走post+user联合索引 const likedRecords = await Like.find({ user: mongoose.Types.ObjectId(currentUserId), post: { $in: postIdList } }).select('post').lean(); // 提取点赞过的Post ID集合,加速判断 const likedPostIdSet = new Set(likedRecords.map(record => record.post.toString())); // 第三步:内存拼接点赞状态 postList.forEach(post => { post.isLiked = likedPostIdSet.has(post._id.toString()); });
这种方案的优势是代码逻辑直观,容易调试和扩展额外业务逻辑,在单页拉取数量不超过200的场景下,性能和聚合查询几乎没有差异。
内容的提问来源于stack exchange,提问作者Raghav Mishra
相关产品推荐
相关产品推荐

