You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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为字符串类型,同时不确定过滤逻辑编写是否正确。


可行解决方案

你之前的逻辑存在两个核心问题:

  1. 字段匹配错误:userSchema中movies数组子文档里,关联电影ID的字段名是movie,返回结果里的movies._id是Mongoose为数组子文档自动生成的唯一标识,不是关联的电影ID,用这个字段做匹配永远无法得到正确结果。
  2. 类型不匹配:子文档中存储的电影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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 01:54:32