当然有几种方法,具体取决于您可用的 MongoDB 版本。这些因不同用途而异$lookup http://mongoosejs.com/docs/api.html#query_Query-populate通过启用对象操作.populate() http://mongoosejs.com/docs/api.html#query_Query-populate结果通过.lean() http://mongoosejs.com/docs/api.html#query_Query-populate.
我确实要求您仔细阅读这些部分,并注意在考虑您的实施解决方案时,一切可能并不像看起来那样。
MongoDB 3.6,“嵌套”$lookup
使用 MongoDB 3.6$lookup https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/操作员获得额外的能力,包括pipeline
表达式而不是简单地将“本地”键值连接到“外部”键值,这意味着您基本上可以执行每个操作$lookup https://docs.mongodb.com/manual/reference/operator/aggregation/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 https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/目标输出可能是一个“数组”,但在“子管道”中,我们可以将该单个元素数组重新整形为单个值。
标准 MongoDB $lookup
仍然保留“在服务器上加入”,您实际上可以这样做$lookup https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/,但只需要中间处理。这是解构数组的长期存在的方法$unwind https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/以及使用$group https://docs.mongodb.com/manual/reference/operator/aggregation/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 https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/ and $unwind https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/当您在每个数组中前进时。
The "author"
当然,细节是单一的,因此一旦“展开”,您只需保留这种方式,进行字段添加并开始“回滚”到数组中的过程。
只有two重建回原来的水平Venue
文档,所以第一个详细级别是Review
重建"comments"
大批。你需要做的就是$push https://docs.mongodb.com/manual/reference/operator/aggregation/push/的路径"$reviews.comments"
为了收集这些,并且只要"$reviews._id"
字段位于“grouping _id”中,您唯一需要保留的其他内容是所有其他字段。您可以将所有这些放入_id
也可以,或者你可以使用$first https://docs.mongodb.com/manual/reference/operator/aggregation/first/.
完成后就只剩下一个了$group https://docs.mongodb.com/manual/reference/operator/aggregation/group/阶段以便回到Venue
本身。这次的分组键是"$_id"
当然,场地本身的所有属性都使用$first https://docs.mongodb.com/manual/reference/operator/aggregation/first/和剩余的"$review"
详细信息返回到数组中$push https://docs.mongodb.com/manual/reference/operator/aggregation/push/。当然"$comments"
之前的输出$group https://docs.mongodb.com/manual/reference/operator/aggregation/group/成为"review.comments"
path.
处理单个文档及其关系,这并不是那么糟糕。这$unwind https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/管道操作员可以一般来说是一个性能问题,但在这种用法的背景下,它不应该真正造成那么大的影响。
由于数据仍在“连接到服务器上”,因此still比其他剩余替代方案的流量少得多。
JavaScript 操作
当然,这里的另一种情况是,您实际上操作的是结果,而不是更改服务器本身的数据。在most在这种情况下,我会赞成这种方法,因为对数据的任何“添加”可能最好在客户端处理。
当然使用的问题populate() http://mongoosejs.com/docs/api.html#query_Query-populate是虽然它可能'看起来像'一个更加简化的过程,实际上是不是加入以任何方式。全部populate() http://mongoosejs.com/docs/api.html#query_Query-populate实际上是"hide"提交的底层流程multiple查询数据库,然后通过异步处理等待结果。
So the “外貌”join的结果实际上是向服务器发出多个请求然后执行的结果“客户端操纵”将详细信息嵌入到数组中的数据。
所以除此之外明确警告性能特征与服务器相差甚远$lookup https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/,另一个需要注意的是,结果中的“mongoose Documents”实际上并不是需要进一步操作的纯 JavaScript 对象。
因此,为了采用这种方法,您需要添加.lean() http://mongoosejs.com/docs/api.html#query_Query-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();
Now 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() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map返回“字符串”值以与req.user.id
这也是一个字符串(如果不是,那么还要添加.toString()
),因为一般来说通过 JavaScript 代码以这种方式比较这些值更容易。
尽管我需要再次强调它“看起来很简单”,但实际上这是您真正想要避免的系统性能问题,因为这些额外的查询以及服务器和客户端之间的传输会花费大量的处理时间甚至由于请求开销,这也增加了托管提供商之间的实际传输成本。
Summary
这些基本上是您可以采取的方法,除了您实际执行的“自行实施”之外“多次查询”自己访问数据库而不是使用帮助程序.populate() http://mongoosejs.com/docs/api.html#query_Query-populate is.
使用填充输出,您可以像任何其他数据结构一样简单地操作结果中的数据,只要您应用.lean() http://mongoosejs.com/docs/api.html#query_Query-lean到查询以转换或以其他方式从返回的猫鼬文档中提取纯对象数据。
虽然综合方法看起来复杂得多,但也有"a lot"在服务器上完成这项工作有更多优势。可以对更大的结果集进行排序,可以进行计算以进一步过滤,当然你会得到一个“单一回应” to a “单一请求”发送到服务器,所有这些都没有额外的开销。
管道本身可以简单地基于已经存储在模式上的属性来构建,这是完全有争议的。因此,编写自己的方法来根据附加模式执行此“构造”应该不会太困难。
当然从长远来看$lookup https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/是更好的解决方案,但是您可能需要在初始编码中投入更多的工作,当然,如果您不只是简单地从此处列出的内容进行复制;)