没有$ unwind的$ lookup多个关卡? [英] $lookup multiple levels without $unwind?

查看:90
本文介绍了没有$ unwind的$ lookup多个关卡?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下收藏

场地收藏

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

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

Now My following populate query works fine

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

But I want to achieve it with $lookup query but it splits the venue when I am doing '$unwind' to the reviews... I want reviews in same array (like populate) and in same order...

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

I want to achieve following query with $lookup because author have followers field so I need to send field isFollow by doing $project which cannot be done using populate...

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

推荐答案

当然,有两种方法取决于您可用的MongoDB版本.从 $lookup 的不同用法到在<通过

There are a couple of approaches of course depending on your available MongoDB version. These vary from different usages of $lookup through to enabling object manipulation on the .populate() result via .lean().

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

I do ask that you read the sections carefully, and be aware that all may not be as it seems when considering your implementation solution.

在MongoDB 3.6中, $lookup 运算符将获得额外的能够包含pipeline表达式,而不是简单地将本地"与外部"键值连接在一起,这意味着您基本上可以执行每个

With MongoDB 3.6 the $lookup operator gets the additional ability to include a pipeline expression as opposed to simply joining a "local" to "foreign" key value, what this means is you can essentially do each $lookup as "nested" within these pipeline expressions

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"数组添加内容,然后每个后续的嵌套"管道表达式也只会看到它的内部" "中的元素.

This can be really quite powerful, as you see from the perspective of the original pipeline, it really only knows about adding content to the "reviews" array and then each subsequent "nested" pipeline expression also only ever sees it's "inner" elements from the join.

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

It is powerful and in some respects it may be a bit clearer as all field paths are relative to the nesting level, but it does start that indentation creep in the BSON structure, and you do need to be aware of whether you are matching to arrays or singular values in traversing the structure.

请注意,我们还可以在"comments"数组条目中看到类似扁平化作者属性"的功能.所有 $lookup 目标输出都可以是数组",但是在子管道"中,我们可以将单个元素数组重塑为单个值.

Note we can also do things here like "flattening the author property" as seen within the "comments" array entries. All $lookup target output may be an "array", but within a "sub-pipeline" we can re-shape that single element array into just a single value.

您仍然可以使用 $lookup ,但它只需要进行中间处理.这是使用 $unwind 来解构数组的长期方法.以及使用 $group 阶段来重建数组:

Still keeping the "join on the server" you can actually do it with $lookup, but it just takes intermediate processing. This is the long standing approach with deconstructing an array with $unwind and the using $group stages to rebuild arrays:

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

这确实不像您一开始想的那样令人生畏,并且遵循 $unwind 当您遍历每个数组时.

This really is not as daunting as you might think at first and follows a simple pattern of $lookup and $unwind as you progress through each array.

"author"的详细信息当然是单数的,因此一旦解开",您只想以这种方式保留它,添加字段并开始回滚"到数组的过程即可.

The "author" detail of course is singular, so once that is "unwound" you simply want to leave it that way, make the field addition and start the process of "rolling back" into the arrays.

只有两个级别可以重建回原始的Venue文档,因此第一个详细信息级别是通过Review来重建"comments"数组.您只需要 $push 以便收集这些,并且只要"$reviews._id"字段位于"grouping _id"中,您需要保留的其他所有内容就是所有其他字段.您也可以将所有这些都放入_id中,或者可以使用 $first .

There are only two levels to reconstruct back to the original Venue document, so the first detail level is by Review to rebuild the "comments" array. All you need to is to $push the path of "$reviews.comments" in order to collect these, and as long as the "$reviews._id" field is in the "grouping _id" the only other things you need to keep are all the other fields. You can put all of these into the _id as well, or you can use $first.

完成此操作后,只有一个 $group 以便返回Venue本身.这次的分组键当然是"$_id",场所本身的所有属性都使用详细信息将通过 $push .当然,先前 $group "$comments"输出将变为"review.comments"路径.

With that done there is only one more $group stage in order to get back to Venue itself. This time the grouping key is "$_id" of course, with all properties of the venue itself using $first and the remaining "$review" details going back into an array with $push. Of course the "$comments" output from the previous $group becomes the "review.comments" path.

在处理单个文档及其关系时,这并不是很糟糕. $unwind 管道运算符通常可以 是一个性能问题,但是在这种用法的背景下,它实际上并不会造成太大的影响.

Working on a single document and it's relations, this is not really so bad. The $unwind pipeline operator can generally be a performance issue, but in the context of this usage it should not really cause that much of an impact.

由于数据仍在连接到服务器上",因此 still 流量要比其余的备用流量少得多.

Since the data is still being "joined on the server" there is still far less traffic than the other remaining alternative.

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

Of course the other case here is that instead of changing data on the server itself, you actually manipulate the result. In most cases I would be in favor of this approach since any "additions" to the data are probably best handled on the client.

使用 populate() 当然会出现问题看起来像" 更加简化了,实际上,没有加入. populate() 实际所做的只是隐藏" 多个查询提交到数据库,然后通过异步处理等待结果的基本过程.

The problem of course with using populate() is that whilst it may 'look like' a much more simplified process, it is in fact NOT A JOIN in any way. All populate() actually does is "hide" the underlying process of submitting multiple queries to the database, and then awaiting the results through async handling.

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

So the "appearance" of a join is actually the result of multiple requests to the server and then doing "client side manipulation" of the data to embed the details within arrays.

除了明确警告之外,性能特征还远远不能与服务器

So aside from that clear warning that the performance characteristics are nowhere close to being on par with a server $lookup, the other caveat is of course that the "mongoose Documents" in the result are not actually plain JavaScript objects subject to further manipulation.

因此,为了采用这种方法,您需要添加 .lean() 执行前查询的方法,以便指示猫鼬返回普通JavaScript对象"而不是使用附加到模型的架构方法强制转换的Document类型.当然要注意,结果数据不再可以访问任何将与相关模型本身相关联的实例方法":

So in order to take this approach, you need to add the .lean() method to the query before execution, in order to instruct mongoose to return "plain JavaScript objects" instead of Document types which are cast with schema methods attached to the model. Noting of course that the resulting data no longer has access to any "instance methods" that would otherwise be associated with the related models themselves:

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

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

Now venue is a plain object, we can simply process and adjust as needed:

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

因此,实际上只需要循环遍历每个内部数组,直到可以在author详细信息中看到followers数组的级别.然后,可以先使用req.user.id进行比较(如果不是,则还要在其上添加.toString()),因为通常这样更容易通过JavaScript代码比较这些值.

So it's really just a matter of cycling through each of the inner arrays down until the level where you can see the followers array within the author details. The comparison then can be made against the ObjectId values stored in that array after first using .map() to return the "string" values for comparison against the req.user.id which is also a string (if it is not, then also add .toString() on that ), since it is easier in general to compare these values in this way via JavaScript code.

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

Again though I need to stress that it "looks simple" but it is in fact the sort of thing you really want to avoid for system performance, as those additional queries and the transfer between the server and the client cost a lot in time of processing and even due to the request overhead this adds up to real costs in transport between hosting providers.

基本上,您可以采用这些方法,而不是通过滚动自己"来实际对数据库执行多个查询" ,而不是使用

Those are basically your approaches you can take, short of "rolling your own" where you actually perform the "multiple queries" to the database yourself instead of using the helper that .populate() is.

使用填充输出,然后您就可以像应用任何其他数据结构一样简单地操作结果中的数据,只要您应用 .lean() 转换为查询,以从返回的猫​​鼬文档中转换或提取普通对象数据.

Using the populate output, you can then simply manipulate the data in result just like any other data structure, as long as you apply .lean() to the query to convert or otherwise extract the plain object data from the mongoose documents returned.

虽然聚合方法看起来更加复杂,但是在服务器上进行这项工作还有很多" 的更多优点.可以对更大的结果集进行排序,可以进行计算以进行进一步的过滤,当然,您还会获得对服务器发出的单个请求" 单个响应" ,所有这些都没有额外的开销.

Whilst the aggregate approaches look far more involved, there are "a lot" more advantages to doing this work on the server. Larger result sets can be sorted, calculations can be done for further filtering, and of course you get a "single response" to a "single request" made to the server, all with no additional overhead.

完全可以争论的是,管道本身可以简单地基于已经存储在模式中的属性来构造.因此,编写您自己的方法以根据附加的架构执行此构造"应该不会太困难.

It is totally arguable that the pipelines themselves could simply be constructed based on attributes already stored on the schema. So writing your own method to perform this "construction" based on the attached schema should not be too difficult.

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

In the longer term of course $lookup is the better solution, but you'll probably need to put a little more work into the initial coding, if of course you don't just simply copy from what is listed here ;)

这篇关于没有$ unwind的$ lookup多个关卡?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆