Node.js $lookup多个级别而不使用$unwind?

Node.js $lookup多个级别而不使用$unwind?,node.js,mongodb,mongoose,mongodb-query,aggregation-framework,Node.js,Mongodb,Mongoose,Mongodb Query,Aggregation Framework,我有以下收藏: 场地收藏 评论集 评论集 作者集 现在,下面的填充查询工作正常 const venues = await Venue.findOne({ _id: id.id }) .populate({ path: 'reviews', options: { sort: { createdAt: -1 } }, populate: [ { path: 'author' }, { path:

我有以下收藏:

  • 场地收藏
  • 评论集
  • 评论集
  • 作者集
现在,下面的填充查询工作正常

    const venues = await Venue.findOne({ _id: id.id })
    .populate({
      path: 'reviews',
      options: { sort: { createdAt: -1 } },
      populate: [
        {  path: 'author'  },
        {  path: 'comments', populate: [{ path: 'author' }] }
      ]
    })
然而,我想通过
$lookup
查询来实现它,但是当我对评论执行“$unwind”时,它会分割场地。。。我想在相同的数组(如填充)和相同的顺序审查

我想用
$lookup
实现以下查询,因为作者有followers字段,所以我需要通过执行
$project
发送字段
isFollow
,这是使用
填充
无法完成的

$project: {
    isFollow: { $in: [mongoose.Types.ObjectId(req.user.id), '$followers'] }
}

当然,有几种方法取决于您可用的MongoDB版本。这些方法的用途各不相同,从使用到通过在结果上启用对象操作

我确实要求您仔细阅读这些章节,并意识到在考虑您的实现解决方案时,可能并非所有内容都是如此

MongoDB 3.6,“嵌套”$lookup 使用MongoDB 3.6,操作员可以添加一个
管道
表达式,而不是简单地将“本地”键值连接到“外部”键值,这意味着您可以在这些管道表达式中“嵌套”每个键值

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "let": { "reviews": "$reviews" },
    "pipeline": [
       { "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
       { "$lookup": {
         "from": Comment.collection.name,
         "let": { "comments": "$comments" },
         "pipeline": [
           { "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
           { "$lookup": {
             "from": Author.collection.name,
             "let": { "author": "$author" },
             "pipeline": [
               { "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
               { "$addFields": {
                 "isFollower": { 
                   "$in": [ 
                     mongoose.Types.ObjectId(req.user.id),
                     "$followers"
                   ]
                 }
               }}
             ],
             "as": "author"
           }},
           { "$addFields": { 
             "author": { "$arrayElemAt": [ "$author", 0 ] }
           }}
         ],
         "as": "comments"
       }},
       { "$sort": { "createdAt": -1 } }
     ],
     "as": "reviews"
  }},
 ])
这可能非常强大,正如您从原始管道的角度所看到的,它只知道如何将内容添加到
“reviews”
数组中,然后每个后续的“嵌套”管道表达式也只能从连接中看到它的“内部”元素

它功能强大,在某些方面可能更清晰,因为所有字段路径都是相对于嵌套级别的,但它确实开始了BSON结构中的缩进蠕变,并且您需要知道在遍历结构时是匹配数组还是匹配奇异值

注意,我们也可以在这里做一些事情,如在
的“comments”
数组条目中看到的“展平author属性”。所有目标输出可能是一个“数组”,但在“子管道”中,我们可以将单个元素数组重新塑造为一个值

标准MongoDB$查找 仍然保留“服务器上的连接”,实际上您可以使用它,但它只需要中间处理。这是一种长期存在的解构阵列的方法,使用阶段和阶段来重建阵列:

Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "localField": "reviews",
    "foreignField": "_id",
    "as": "reviews"
  }},
  { "$unwind": "$reviews" },
  { "$lookup": {
    "from": Comment.collection.name,
    "localField": "reviews.comments",
    "foreignField": "_id",
    "as": "reviews.comments",
  }},
  { "$unwind": "$reviews.comments" },
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "reviews.comments.author",
    "foreignField": "_id",
    "as": "reviews.comments.author"
  }},
  { "$unwind": "$reviews.comments.author" },
  { "$addFields": {
    "reviews.comments.author.isFollower": {
      "$in": [ 
        mongoose.Types.ObjectId(req.user.id), 
        "$reviews.comments.author.followers"
      ]
    }
  }},
  { "$group": {
    "_id": { 
      "_id": "$_id",
      "reviewId": "$review._id"
    },
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "review": {
      "$first": {
        "_id": "$review._id",
        "createdAt": "$review.createdAt",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content"
      }
    },
    "comments": { "$push": "$reviews.comments" }
  }},
  { "$sort": { "_id._id": 1, "review.createdAt": -1 } },
  { "$group": {
    "_id": "$_id._id",
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "reviews": {
      "$push": {
        "_id": "$review._id",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content",
        "comments": "$comments"
      }
    }
  }}
])
这并不像您一开始想象的那样令人畏惧,而是遵循一个简单的和模式,随着您在每个阵列中的前进

“author”
详细信息当然是单数的,因此一旦“解开”了,您只需将其保持为单数,添加字段并开始“回滚”到数组中的过程

只有两个级别可以重建回原始的
场馆
文档,因此第一个详细级别是通过
查看
来重建
评论
数组。要收集这些评论,您只需访问
“$reviews.comments”
的路径,只要
“$reviews.id”
字段位于“分组id”中,您只需保留所有其他字段。您也可以将所有这些内容放入
\u id
中,也可以使用

完成后,只有一个阶段才能回到
场馆
本身。这一次分组键当然是
“$\u id”
,场馆本身的所有属性都在使用,剩余的
“$review”
详细信息将返回到一个数组中。当然,前面的
“$comments”
输出将成为
“review.comments”
路径

在处理单个文档及其关系时,这并没有那么糟糕。管道操作符通常可能是一个性能问题,但在这种使用情况下,它不应该真正造成那么大的影响

由于数据仍在“连接到服务器上”,因此通信量仍远低于其他剩余的备选方案

JavaScript操作 当然,这里的另一种情况是,不是更改服务器本身的数据,而是实际操作结果。在大多数情况下,我会支持这种方法,因为对数据的任何“添加”可能最好在客户机上处理

当然,使用的问题是,虽然它看起来“像”一个简化得多的过程,但实际上它在任何方面都不是一个连接。实际上所做的只是“隐藏”向数据库提交多个查询的底层过程,然后通过异步处理等待结果

因此,连接的“外观”实际上是对服务器的多个请求的结果,然后对数据进行“客户端操作”,以将细节嵌入阵列中

因此,除了“强”>“清晰的警告< /强”之外,性能特征与服务器不太接近,另一个警告当然是结果中的“MangoOffice文档”实际上不是纯JavaScript对象,需要进一步操作。 因此,为了采用这种方法,您需要在执行之前将该方法添加到查询中,以便指示mongoose返回“普通JavaScript对象”,而不是使用附加到模型的模式方法强制转换的

Document
类型。当然,注意到结果数据不再具有访问任何“实例方法”的权限,否则这些方法将与相关模型本身相关联:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();
现在
场馆
是一个简单的对象,我们可以根据需要简单地处理和调整:

venue.reviews = venue.reviews.map( r => 
  ({
    ...r,
    comments: r.comments.map( c =>
      ({
        ...c,
        author: {
          ...c.author,
          isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
        }
      })
    )
  })
);
因此,这实际上只是一个循环遍历每个内部数组的问题,直到您可以在
作者
详细信息中看到
追随者
数组的级别。然后可以与存储在该数组a中的
ObjectId
值进行比较
Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "let": { "reviews": "$reviews" },
    "pipeline": [
       { "$match": { "$expr": { "$in": [ "$_id", "$$reviews" ] } } },
       { "$lookup": {
         "from": Comment.collection.name,
         "let": { "comments": "$comments" },
         "pipeline": [
           { "$match": { "$expr": { "$in": [ "$_id", "$$comments" ] } } },
           { "$lookup": {
             "from": Author.collection.name,
             "let": { "author": "$author" },
             "pipeline": [
               { "$match": { "$expr": { "$eq": [ "$_id", "$$author" ] } } },
               { "$addFields": {
                 "isFollower": { 
                   "$in": [ 
                     mongoose.Types.ObjectId(req.user.id),
                     "$followers"
                   ]
                 }
               }}
             ],
             "as": "author"
           }},
           { "$addFields": { 
             "author": { "$arrayElemAt": [ "$author", 0 ] }
           }}
         ],
         "as": "comments"
       }},
       { "$sort": { "createdAt": -1 } }
     ],
     "as": "reviews"
  }},
 ])
Venue.aggregate([
  { "$match": { "_id": mongoose.Types.ObjectId(id.id) } },
  { "$lookup": {
    "from": Review.collection.name,
    "localField": "reviews",
    "foreignField": "_id",
    "as": "reviews"
  }},
  { "$unwind": "$reviews" },
  { "$lookup": {
    "from": Comment.collection.name,
    "localField": "reviews.comments",
    "foreignField": "_id",
    "as": "reviews.comments",
  }},
  { "$unwind": "$reviews.comments" },
  { "$lookup": {
    "from": Author.collection.name,
    "localField": "reviews.comments.author",
    "foreignField": "_id",
    "as": "reviews.comments.author"
  }},
  { "$unwind": "$reviews.comments.author" },
  { "$addFields": {
    "reviews.comments.author.isFollower": {
      "$in": [ 
        mongoose.Types.ObjectId(req.user.id), 
        "$reviews.comments.author.followers"
      ]
    }
  }},
  { "$group": {
    "_id": { 
      "_id": "$_id",
      "reviewId": "$review._id"
    },
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "review": {
      "$first": {
        "_id": "$review._id",
        "createdAt": "$review.createdAt",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content"
      }
    },
    "comments": { "$push": "$reviews.comments" }
  }},
  { "$sort": { "_id._id": 1, "review.createdAt": -1 } },
  { "$group": {
    "_id": "$_id._id",
    "name": { "$first": "$name" },
    "addedBy": { "$first": "$addedBy" },
    "reviews": {
      "$push": {
        "_id": "$review._id",
        "venue": "$review.venue",
        "author": "$review.author",
        "content": "$review.content",
        "comments": "$comments"
      }
    }
  }}
])
let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();
venue.reviews = venue.reviews.map( r => 
  ({
    ...r,
    comments: r.comments.map( c =>
      ({
        ...c,
        author: {
          ...c.author,
          isAuthor: c.author.followers.map( f => f.toString() ).indexOf(req.user.id) != -1
        }
      })
    )
  })
);