node.js - 没有$ unwind的$ lookup多个级别?

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

我有以下收藏:

  • field 集合
  • {    "_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”操作时,它会分割 field ...我希望评论以相同的数组(例如填充)并以相同的顺序...
    我想用$lookup实现以下查询,因为作者具有followers字段,所以我需要通过执行isFollow发送字段$project,而使用populate则无法完成...
    $project: {
        isFollow: { $in: [mongoose.Types.ObjectId(req.user.id), '$followers'] }
    }
    

    最佳答案

    当然,有两种方法取决于您可用的MongoDB版本。从 $lookup 的不同用法到通过 .populate() .lean() 结果启用对象操作,这些方法有所不同。
    我的确要求您仔细阅读本节,并且请注意,考虑实施解决方案时,所有内容可能都不尽相同。
    MongoDB 3.6,“嵌套” $ lookup
    与MongoDB 3.6相比, $lookup 运算符获得了额外的功能来包含pipeline表达式,而不是简单地将“local”与“foreign”键值连接在一起,这意味着您基本上可以在这些管道表达式中将每个 $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", field 本身的所有属性都使用 $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
            }
          })
        )
      })
    );
    
    因此,实际上只需要循环遍历每个内部数组,直到可以在followers详细信息中看到author数组的级别。在首先使用 ObjectId 返回“字符串”值以与也是字符串的.map()比较之后,可以与存储在该数组中的req.user.id值进行比较(如果不是,则还要在其上添加.toString()),因为通常,通过JavaScript代码以这种方式比较这些值比较容易。
    再次强调一下,“它看起来很简单”,但实际上,这是您真正要避免的系统性能问题,因为这些额外的查询以及服务器和客户端之间的传输会花费大量的处理时间甚至由于请求开销,这也增加了托管提供商之间的实际传输成本。

    概要
    基本上,您可以采用这些方法,而无需“滚动自己”,而实际上您自己对数据库执行“多个查询”,而不是使用 .populate() 是辅助程序。
    使用填充输出,然后您就可以像处理任何其他数据结构一样简单地操作结果中的数据,只要将 .lean() 应用于查询即可转换或以其他方式从返回的猫​​鼬文档中提取纯对象数据。
    尽管聚合方法看起来涉及更多,但在服务器上进行此工作还有“很多”优点。可以对更大的结果集进行排序,可以进行计算以进行进一步的过滤,并且当然,您会获得对服务器发出的“单个请求”的“单个响应”,而没有任何额外的开销。
    完全可以争论的是,流水线本身可以简单地基于已经存储在模式中的属性来构造。因此,编写您自己的方法以基于附加模式执行此“构造”应该不太困难。
    从长远来看,当然 $lookup 是更好的解决方案,但是如果您不只是简单地从此处列出的内容中进行复制,那么您可能需要在初始编码中投入更多的工作;)

    关于node.js - 没有$ unwind的$ lookup多个级别?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56216316/

    相关文章:

    javascript - 如果没有值,使用 MongooseJS 填充文档

    node.js - 使用nodejs在mongoose中保存嵌套文档时的错误处理

    node.js - 静态方法的 Mongoose 继承

    javascript - 实现搜索功能 Node JS/MySQL

    node.js - Snowflake DB 与 Node.js 驱动程序的连接

    node.js - 在镜像大小较小的 Docker Alpine 镜像中创建非 root 用户?

    node.js - 如何在 Ubuntu 服务器中部署基于 Node JS 的 API 进行生产

    python - 从字典/列表中删除项目

    python - pymongo DuplicateKeyError - 在 upsert 期间

    mongodb - 如何实现 $bucket 按多个字段分组