MongoDB聚合:字符串_id数组匹配ObjectId过滤失效问题
问题场景
首先定义了如下两个集合的Schema结构:
const movieSchema = new Schema({ title: String ... }) const userSchema = new Schema({ firstName: String, lastName: String, movies: [ movie: { type: Schema.Types.ObjectId, ref: 'Movie' }, status: String, feeling: String ] ... })
需要通过聚合查询,将指定电影的完整详情,与用户对该电影标记的观看状态、感受信息关联匹配,最初编写的聚合管道代码如下:
Movie.aggregate([ { $match: { _id: ObjectId(movieId) } }, { $lookup: { from: 'users', as: 'user_status', pipeline: [ { $match: { _id: ObjectId(userId) } }, { $project: { _id: 0, movies: 1 } }, { $unwind: '$movies' } ] } }, ])
执行上述聚合后返回了用户所有标记过的电影条目,未做对应电影的匹配过滤,结果如下:
[ { _id: 610b678702500b0646925542, title: 'The Shawshank Redemption', user_status: [ { "movies": { "_id": "610b678702500b0646925542", "status": "watched", "feeling": "love" } }, { "movies": { "_id": "610b678502500b0646923627", "status": "watched", "feeling": "like" } }, { "movies": { "_id": "610b678502500b0646923637", "status": "watched", "feeling": "like" } }, ] } ]
期望得到的最终结果是仅匹配当前查询电影对应的用户状态、感受,返回结构如下:
[ { _id: 610b678702500b0646925542, title: 'The Shawshank Redemption', status: "watched", feeling: "love" } ]
曾尝试在聚合管道中追加如下$addFields阶段,使用$filter过滤user_status数组中匹配当前电影_id的项,但逻辑未生效:
{ $addFields: { user_status: { $filter: { input: '$user_status', cond: { $eq: ['$$this.movies._id', '$_id'] } } } } }
已知的问题是根文档的_id为ObjectId类型,而user_status数组内子项movies._id为字符串类型,同时不确定过滤逻辑编写是否正确。
可行解决方案
你之前的逻辑存在两个核心问题:
- 字段匹配错误:
userSchema中movies数组子文档里,关联电影ID的字段名是movie,返回结果里的movies._id是Mongoose为数组子文档自动生成的唯一标识,不是关联的电影ID,用这个字段做匹配永远无法得到正确结果。 - 类型不匹配:子文档中存储的电影ID和根文档的
_id类型不一致,比较前需要做类型转换。
以下提供两种实现方案,优先选择第一种,性能更好:
方案1:优化$lookup子管道,直接过滤匹配数据
不需要拉取用户所有电影标记再做二次过滤,直接在$lookup的子管道中完成匹配和字段投影,减少冗余数据传输:
Movie.aggregate([ { $match: { _id: ObjectId(movieId) } }, { $lookup: { from: 'users', as: 'userMovieInfo', let: { currentMovieId: '$_id' }, // 传递当前电影ID到子管道 pipeline: [ { $match: { _id: ObjectId(userId) } }, { $unwind: '$movies' }, // 展开用户的电影标记数组 { $match: { // 转换ID类型后匹配当前电影 $expr: { $eq: [ { $toObjectId: '$movies.movie' }, '$$currentMovieId' ] } }}, { $project: { _id: 0, status: '$movies.status', feeling: '$movies.feeling' }} ] } }, // 将匹配到的状态、感受提取为根文档字段 { $addFields: { status: { $first: '$userMovieInfo.status' }, feeling: { $first: '$userMovieInfo.feeling' } }}, // 移除临时字段 { $project: { userMovieInfo: 0 } } ])
方案2:保留原有lookup逻辑,修复过滤规则
如果不想调整原有$lookup结构,只需要修正过滤条件、补充类型转换,再提取对应字段即可:
// 在原有$lookup阶段后追加以下管道阶段 { $addFields: { user_status: { $filter: { input: '$user_status', cond: { // 用正确的关联字段+类型转换做匹配 $eq: [ { $toObjectId: '$$this.movies.movie' }, '$_id' ] } } } } }, // 提取状态、感受到根级 { $addFields: { status: { $first: '$user_status.movies.status' }, feeling: { $first: '$user_status.movies.feeling' } } }, // 移除临时字段 { $project: { user_status: 0 } }
两种方案最终都会返回你期望的结构,方案1在数据量较大时性能优势更明显。
内容的提问来源于stack exchange,提问作者gregg
相关产品推荐
相关产品推荐

