我有以下收藏:
{ "_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
实现以下查询因为作者有关注者字段所以我需要发送字段 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
的额外能力表达式与简单地将“本地”键值连接到“外部”键值相反,这意味着您基本上可以执行每个 $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"
当然, field 本身的所有属性都使用 $first
剩下的"$review"
返回到数组的详细信息 $push
.当然是"$comments"
上一个 $group
的输出成为 "review.comments"
小路。处理单个文档及其关系,这并不是那么糟糕。
$unwind
管道运算符通常可能是一个性能问题,但在这种用法的上下文中,它不应该真正造成那么大的影响。由于数据仍在“加入服务器”,因此与其他剩余替代方案相比,流量仍然要少得多。
JavaScript 操作
当然,这里的另一种情况是,您实际上是在操纵结果,而不是更改服务器本身上的数据。在大多数情况下,我会赞成这种方法,因为对数据的任何“添加”可能最好在客户端处理。
问题当然是使用
populate()
是虽然它可能“看起来”是一个更简化的过程,但实际上它是 不是加入 以任何方式。全部 populate()
实际上确实是“隐藏”了提交的底层流程多个 查询到数据库,然后通过异步处理等待结果。因此,连接的“外观”实际上是对服务器的多个请求的结果,然后对数据进行“客户端操作”以将详细信息嵌入到数组中。
除此之外清除警告 性能特征远不及服务器
$lookup
,另一个警告当然是结果中的“ Mongoose 文档”实际上并不是可以进一步操作的普通 JavaScript 对象。因此,为了采用这种方法,您需要添加
.lean()
方法在执行前添加到查询中,以指示 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
是一个普通对象,我们可以根据需要进行简单的处理和调整: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/49953780/