一尘不染

没有$ unwind的$ lookup多个级别?

node.js

我有以下收藏

场地集合

{
    "_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”操作时,它会分割场地…我希望评论以相同的数组(例如填充)和相同的顺序…

我想实现以下查询,$lookup因为作者具有followers字段,所以我需要通过发送来发送字段isFollow$project而使用populate

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

阅读 295

收藏
2020-07-07

共1个答案

一尘不染

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

我的确要求您仔细阅读本节,并且请注意,考虑实施解决方案时,所有内容可能都不尽相同。

MongoDB 3.6,“嵌套” $ lookup

使用MongoDB
3.6,$lookup操作员可以获得额外的能力来包含pipeline表达式,而不是简单地将“本地”键与“外部”键值连接在一起,这意味着您基本上可以$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"字段位于“
grouping
_id”中,您需要保留的其他所有内容就是所有其他字段。您也可以将所有这些都放入_id,也可以使用$first

完成此操作后,只有一个$group阶段可以恢复到原来状态Venue。这次"$_id"当然是分组键了,场地本身的所有属性都在使用$first,其余的"$review"细节又用放回到数组中$push。当然"$comments",前一个输出将$group成为"review.comments"路径。

处理单个文档及其关系,这并不是很糟糕。该$unwind管道运营商可以
一般 是一个性能问题,但在这使用的情况下,应该不是真的会导致多大的冲击。

由于数据仍在“连接到服务器上”,因此流量 仍然 远远少于其他方式。

JavaScript操作

当然,这里的另一种情况是,您实际上没有操纵服务器上的数据,而是操纵了结果。在 大多数
情况下,我会赞成这种方法,因为对数据的任何“添加”都可能最好在客户端上进行处理。

使用过程中的问题当然populate()是,尽管它 看起来 可能 像是 一个简化得多的过程,但实际上 绝不是一种联接
。所有populate()实际上做的是 “隐藏” 提交的基本过程 的多个 数据库查询,然后等待通过异步处理结果。

因此,联接的 “外观” 实际上是对服务器的多个请求的结果,然后对数据进行 “客户端操作” 以将详细信息嵌入到数组中。

因此,除了 明确的警告
,即性能特征远不能与服务器相提并论之外$lookup,另一个警告是,结果中的“猫鼬文档”当然不是经过进一步处理的普通JavaScript对象。

因此,为了采用这种方法,您需要.lean()在执行之前将方法添加到查询中,以指示猫鼬返回“普通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是一个普通的对象,我们可以根据需要简单地进行处理和调整:

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

因此,实际上只需要循环遍历每个内部数组,直到可以followersauthor细节中看到该数组的级别为止。然后,可以先与该ObjectId数组中存储的.map()值进行比较,之后首先使用它来返回“字符串”值以与req.user.id也是字符串的字符串进行比较(如果不是,则还要在其.toString()上加上),因为这样做更容易通常通过JavaScript代码以这种方式比较这些值。

再次强调一下,“它看起来很简单”,但实际上,这是您真正要避免的系统性能问题,因为这些额外的查询以及服务器和客户端之间的传输会花费大量的处理时间甚至由于请求开销,这也增加了托管提供商之间的实际传输成本。


摘要

这些基本上就是您可以采用的方法,而无需“滚动自己”,而您实际上是自己对数据库执行 “多个查询”
,而不是使用帮助.populate()器。

使用填充输出,然后您就可以像处理任何其他数据结构一样简单地操作结果中的数据,只要您向.lean()查询申请转换或以其他方式从返回的猫​​鼬文档中提取纯对象数据即可。

尽管聚合方法看起来更加复杂,但是在服务器上进行此工作还有 “很多”
优点。可以对更大的结果集进行排序,可以进行计算以进行进一步的过滤,并且当然,您会获得对服务器的 “单个请求”“单个响应”
,而没有任何额外的开销。 __

完全可以争论的是,流水线本身可以简单地基于已经存储在模式中的属性来构造。因此,编写您自己的方法以基于附加模式执行此“构造”应该不太困难。

从长远来看,当然$lookup是更好的解决方案,但是如果您不只是简单地复制此处列出的内容,那么您可能需要在初始编码中投入更多的工作;)

2020-07-07