如何避免在forEach循环中调用数据库?Express+MongoDB聊天应用优化
优化聊天应用用户列表的消息关联查询
我正在开发一款聊天应用,后端用Express JS + MongoDB,前端是Angular。现有Users和ChatHistory两个集合,Schema如下:
Users集合Schema
const UserSchema = new mongoose.Schema({ name:{type: type.String, required: true}, contact:{type: type.String, required: true}, email:{type: type.String, required: true}, password:{type: type.String, required: true}, age:{type: type.Number, default:0}, pic:{type: type.String, default:''}, dob: {type:type.Date, default: ''}, last_seen: {type:type.Date, default: ''}, online_status: {type:type.String, default: 'offline'}, show_last_seen: {type:type.Boolean, default: true}, })
ChatHistory集合Schema
const ChatHistorySchema = new mongoose.Schema({ sender_id:{type: type.ObjectId, ref:'Users', required: true}, // 修正原拼写错误:Usres → Users receiver_id:{type: type.ObjectId, ref:'Users',required: true}, message:{type: type.String,required: true}, is_deleted:{type: type.Boolean,default:false}, target_messages_id:{type:type.String,default:''}, file_name:{type:type.String,default:''}, message_read_status:{type: type.Boolean,default:false}, })
我需要在展示用户列表时,同时显示每个用户的最后接收消息和未读消息总数。当前实现是先查询所有用户,再循环每个用户调用数据库查询消息,代码如下:
userList = await UsersCollection.find({},{name:1,email:1,online_status:1,last_seen:1,pic:1}); //获取所有用户列表 if(userList.length > 0){ //遍历每个用户,获取其最后消息与未读消息总数 async forEach((obj)=>{ await getLastMessage(); await getAllUnreadMessageCount() }) }
这种循环查库的方式性能很差,有没有更优的实现方案?
优化方案:使用MongoDB聚合查询(Aggregation Pipeline)
直接通过聚合管道将用户数据与聊天记录关联,一次性查询出所有需要的信息,彻底避免N+1查询问题。核心逻辑是:
- 过滤排除当前登录用户
- 关联聊天记录,提取每个用户与当前用户对话的最后一条消息
- 统计当前用户收到的来自该用户的未读消息数
- 合并数据并返回需要的字段
具体代码实现(假设当前登录用户ID为currentUserId)
const currentUserId = req.user._id; // 从登录态获取当前用户ID const userListWithMessages = await UsersCollection.aggregate([ // 排除当前用户自己 { $match: { _id: { $ne: currentUserId } } }, // 关联聊天记录,获取与当前用户的最后一条对话 { $lookup: { from: "chathistories", // MongoDB默认集合名为小写复数 let: { userId: "$_id" }, pipeline: [ { $match: { $expr: { $or: [ { $and: [{ $eq: ["$sender_id", "$$userId"] }, { $eq: ["$receiver_id", currentUserId] }] }, { $and: [{ $eq: ["$receiver_id", "$$userId"] }, { $eq: ["$sender_id", currentUserId] }] } ] }, is_deleted: false // 排除已删除消息 } }, { $sort: { _id: -1 } }, // 按消息生成时间降序(ObjectId自带时间戳) { $limit: 1 }, // 取最后一条消息 { $project: { last_message: "$message", last_message_time: "$_id" } } ], as: "last_chat" } }, // 提取最后消息的具体字段(将数组转为单个值) { $addFields: { last_message: { $arrayElemAt: ["$last_chat.last_message", 0] }, last_message_time: { $arrayElemAt: ["$last_chat.last_message_time", 0] } } }, { $unset: "last_chat" }, // 移除临时数组字段 // 关联未读消息统计 { $lookup: { from: "chathistories", let: { userId: "$_id" }, pipeline: [ { $match: { $expr: { $and: [ { $eq: ["$sender_id", "$$userId"] }, { $eq: ["$receiver_id", currentUserId] } ] }, message_read_status: false, is_deleted: false } }, { $count: "unread_count" } ], as: "unread_info" } }, // 处理未读消息数,默认0 { $addFields: { unread_count: { $ifNull: [{ $arrayElemAt: ["$unread_info.unread_count", 0] }, 0] } } }, { $unset: "unread_info" }, // 保留需要返回的用户字段 { $project: { name: 1, email: 1, online_status: 1, last_seen: 1, pic: 1, last_message: 1, last_message_time: 1, unread_count: 1 } } ]);
额外性能优化建议
给ChatHistory集合建立复合索引,提升聚合查询速度:
ChatHistorySchema.index({ sender_id: 1, receiver_id: 1, message_read_status: 1, is_deleted: 1 });
内容的提问来源于stack exchange,提问作者Irshad Khan
相关产品推荐
相关产品推荐

