JavaScript配合MongoDB开发时循环给对象新增属性失效如何解决
问题核心原因
- Mongoose的
find查询默认返回的是Mongoose Document实例,默认开启严格模式,未在Schema中定义的属性不会被序列化输出,所以即使手动挂载了votes属性,返回给前端时也会被过滤。 forEach和map不会等待内部的async函数执行完成,你通过判断索引是否等于数组长度来返回响应的逻辑不可靠:异步查询的返回顺序是不确定的,很可能最后一个索引的查询先完成,此时前面的用户还没统计完投票数就提前返回了。
正确实现方案
基础修复版
查询时添加.lean()方法直接返回普通JS对象,配合Promise.all等待所有异步统计操作完成后再统一返回:
// 加lean()直接返回普通对象,可自由添加属性,同时查询性能更高 const users = await User.find( { teams: { $in: req.user.team_oid } }, { _id: 1, firstname: 1, lastname: 1 } ).lean() // 用Promise.all等待所有投票统计完成 const usersWithVotes = await Promise.all( users.map(async user => { const voteCount = await Survey.countDocuments({ _id: mongoose.Types.ObjectId(req.params.s_oid), 'facilitators.nominee': mongoose.Types.ObjectId(user._id) }) user.votes = voteCount return user }) ) // 所有操作完成后统一返回 res.json({ users: usersWithVotes })
如果不想用lean(),也可以在挂载属性前手动将Document转成普通对象:
const userObj = user.toObject() userObj.votes = voteCount return userObj
性能优化版
上面的方案会产生N+1次数据库请求(1次查用户,N次查每个用户的投票数),如果用户数量较多,推荐用聚合查询一次性关联统计所有用户的投票数,只需要2次数据库请求即可完成:
// 先统计所有符合条件的用户投票数 const voteStats = await Survey.aggregate([ { $match: { _id: mongoose.Types.ObjectId(req.params.s_oid) } }, { $unwind: '$facilitators' }, { $group: { _id: '$facilitators.nominee', votes: { $sum: 1 } }} ]) // 把统计结果转成映射表方便匹配 const voteMap = new Map(voteStats.map(item => [item._id.toString(), item.votes])) // 查询用户后直接匹配投票数 const users = await User.find( { teams: { $in: req.user.team_oid } }, { _id: 1, firstname: 1, lastname: 1 } ).lean() const usersWithVotes = users.map(user => ({ ...user, votes: voteMap.get(user._id.toString()) || 0 })) res.json({ users: usersWithVotes })
内容的提问来源于stack exchange,提问作者Izzi
相关产品推荐
相关产品推荐

