$查找多个级别而不需要$ unwind?

时间:2018-04-21 08:20:08

标签: node.js mongodb mongoose mongodb-query aggregation-framework

我有以下收藏品

场地收藏

{
    "_id" : ObjectId("5acdb8f65ea63a27c1facf86"),
    "name" : "ASA College - Manhattan Campus",
    "addedBy" : ObjectId("5ac8ba3582c2345af70d4658"),
    "reviews" : [ 
        ObjectId("5acdb8f65ea63a27c1facf8b"), 
        ObjectId("5ad8288ccdd9241781dce698")
    ]
}

评论收藏

{
    "_id" : ObjectId("5acdb8f65ea63a27c1facf8b"),
    "createdAt" : ISODate("2018-04-07T12:31:49.503Z"),
    "venue" : ObjectId("5acdb8f65ea63a27c1facf86"),
    "author" : ObjectId("5ac8ba3582c2345af70d4658"),
    "content" : "nice place",
    "comments" : [ 
        ObjectId("5ad87113882d445c5cbc92c8")
    ],
}

评论收集

{
    "_id" : ObjectId("5ad87113882d445c5cbc92c8"),
    "author" : ObjectId("5ac8ba3582c2345af70d4658"),
    "comment" : "dcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsf",
    "review" : ObjectId("5acdb8f65ea63a27c1facf8b"),
    "__v" : 0
}

作者集合

{
    "_id" : ObjectId("5ac8ba3582c2345af70d4658"),
    "firstName" : "Bruce",
    "lastName" : "Wayne",
    "email" : "bruce@linkites.com",
    "followers" : [ObjectId("5ac8b91482c2345af70d4650")]
}

现在我的以下填充查询工作正常

    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'评论......我希望评论相同的数组(如populate)和相同的顺序......

我希望使用$lookup来实现以下查询,因为作者有关注者字段,因此我需要通过执行isFollow来发送字段$project,这是使用populate无法完成的。

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

1 个答案:

答案 0 :(得分:15)

当然,有几种方法取决于您可用的MongoDB版本。这些内容从$lookup的不同用法到通过.populate().lean()结果启用对象操作有所不同。

我确实要求您仔细阅读这些部分,并注意在考虑实施解决方案时,所有部分可能都不一样。

MongoDB 3.6,"嵌套" $查找

使用MongoDB 3.6,$lookup运算符可以获得包含pipeline表达式的额外功能,而不是简单地加入" local"到"外国"关键值,这意味着你基本上可以将每个$lookup作为"嵌套"在这些管道表达式中

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"数组条目中所示。所有$lookup目标输出可能是一个"数组"但是在一个"子流水线"我们可以将单个元素数组重新塑造成一个单独的值。

标准MongoDB $ lookup

仍然保持"加入服务器"你可以用$lookup实际做到这一点,但它只需要中间处理。这是使用$unwind解析数组并使用$group阶段重建数组的长期方法:

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"
      }
    }
  }}
])

这一点并不像您最初想象的那样令人生畏,并且在您逐步浏览每个数组时遵循一个简单的$lookup$unwind模式。

"author"详细信息当然是单数的,所以一旦这是"解开"你只是想以这种方式离开,加上字段,并开始回滚"回滚"进入阵列。

只有两个级别可以重建回原始Venue文档,因此第一个详细信息级别由Review重建"comments"数组。您需要的只是$push "$reviews.comments"的路径才能收集这些内容,只要"$reviews._id"字段位于"分组_id"你需要保留的唯一其他事情是所有其他领域。您也可以将所有这些内容放入_id,也可以使用$first

完成此操作后,只有一个$group阶段才能返回Venue本身。这次分组键当然是"$_id",场地的所有属性都使用$first,剩余的"$review"详细信息会返回到$push的数组中。当然,前一个$group"$comments"输出成为"review.comments"路径。

处理单个文档及其关系,这并不是真的那么糟糕。 $unwind管道运算符通常可能是性能问题,但在此用法的上下文中,它不应该真正造成太大的影响。

由于数据仍在服务器上加入" 仍然远远少于其他剩余的替代方案。

JavaScript操作

当然,另一种情况是,您实际上操纵结果,而不是更改服务器本身的数据。在大多数案例中,我会支持这种方法,因为任何"添加"可能最好在客户端处理数据。

使用populate()的当然问题是,虽然它可能看起来像是一个更加简化的过程,但实际上不是A以任何方式加入。所有populate()实际上都是"隐藏" 向数据库提交多个查询的基础流程,然后通过异步处理等待结果

因此,连接的&#34;外观&#34; 实际上是对服务器的多个请求然后执行&#34;客户端操作&#34; <的结果/ em>将数据嵌入数组中的数据。

除了明确警告之外,性能特征与服务器$lookup无法接近,另一个警告当然是&#34; mongoose Documents& #34;在结果中实际上不是普通的JavaScript对象,需要进一步操作。

因此,为了采用这种方法,您需要在执行前将.lean()方法添加到查询中,以指示mongoose返回&#34;纯JavaScript对象&#34;而不是使用附加到模型的模式方法强制转换的Document类型。当然注意到结果数据不再能访问任何&#34;实例方法&#34;否则将与相关模型本身相关联:

let venue = await Venue.findOne({ _id: id.id })
  .populate({ 
    path: 'reviews', 
    options: { sort: { createdAt: -1 } },
    populate: [
     { path: 'comments', populate: [{ path: 'author' }] }
    ]
  })
  .lean();

现在venue是一个普通的对象,我们可以根据需要简单地处理和调整:

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
        }
      })
    )
  })
);

因此,只需循环浏览每个内部数组,直到可以在followers详细信息中看到author数组的级别。然后,可以在首次使用.map()返回&#34;字符串&#34;之后,对照存储在该数组中的ObjectId值进行比较。用于与req.user.id进行比较的值,也是一个字符串(如果不是,那么也在其上添加.toString()),因为通常可以通过JavaScript代码以这种方式比较这些值。

虽然我需要强调它看起来很简单&#34;但它实际上是你真正想要避免的系统性能,因为那些额外的查询以及服务器和客户端之间的转移在处理时花费了很多,甚至由于请求开销而增加了实际托管服务提供商之间的运输成本。

摘要

这些基本上是你可以采取的方法,而不是&#34;滚动你自己的&#34;您实际执行&#34;多个查询&#34; 的地方,而不是使用.populate()的帮助程序。

使用填充输出,您可以像处理任何其他数据结构一样简单地操作结果中的数据,只要您将.lean()应用于查询以转换或以其他方式从mongoose文档中提取普通对象数据返回。

虽然聚合方法看起来涉及的更多,但在服务器上执行此工作有很多优点和更多优势。 。可以对较大的结果集进行排序,可以进行计算以进一步过滤,当然,您可以获得&#34;单个响应&#34; &#34;单个请求&#34 ; 发送到服务器,所有这些都没有额外的开销。

完全有争议的是,管道本身可以简单地基于已经存储在模式中的属性来构造。所以编写自己的方法来执行此操作&#34;构建&#34;基于附加的模式应该不会太困难。

从长远来看,$lookup是更好的解决方案,但是你可能需要在初始编码中加入更多的工作,如果你不是简单地复制从这里列出的内容;)