当然,具体取决于您可用的MongoDB版本,有几种方法。这些方法从不同用途的$lookup
使用到通过.lean()
在.populate()
结果上启用对象操作。
我要求您仔细阅读各节,并注意在考虑实现解决方案时,一切可能并非看起来那么简单。
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"
字段在“分组_id”中,你需要保留的其他字段就是所有其他字段。你也可以把所有这些字段都放到_id
中,或者你可以使用$first
。
完成这一步骤后,只需再进行一个
$group
阶段即可返回到
Venue
本身。这次分组关键字当然是使用
"$_id"
,使用
$first
将场馆本身的所有属性提取出来,并使用
$push
将剩余的
"$review"
细节放回一个数组中。当然,上一个
$group
的输出
"$comments"
成为了
"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
}
})
)
})
);
所以,这只是在每个内部数组中循环,直到您可以看到
author
详细信息中的
followers
数组的级别。然后,可以使用
.map()
返回用于与该数组中存储的
ObjectId
值进行比较的“字符串”值,首先将其与
req.user.id
进行比较,后者也是一个字符串(如果不是,则还需添加
.toString()
),因为通常使用JavaScript代码以这种方式比较这些值更容易。
但再次强调,这“看起来简单”,但实际上这是您真正想要避免系统性能问题的事情,因为那些额外的查询和服务器和客户端之间的传输会花费大量处理时间,甚至由于请求开销,这增加了在托管提供商之间的传输的实际成本。
总结
除了自己编写代码实际执行多个查询而不使用.populate()
帮助程序之外,这些基本上是您可以采取的方法。
使用填充输出,只要将.lean()
应用于查询以从返回的mongoose文档中转换或提取纯对象数据,就可以像任何其他数据结构一样简单地操作结果。
虽然聚合方法看起来更加复杂,但在服务器上执行此工作有更多优势。可以对更大的结果集进行排序,可以进行计算以进行进一步过滤,并且当然您会得到一个“单个请求”向服务器发出的“单个响应”,所有这些都没有额外的开销。
可以完全争论管道本身是否可以基于已存储在模式上的属性进行构建。因此,根据所附的模式编写自己的方法来执行此“构建”可能不太困难。
长期来看,$lookup
是更好的解决方案,但如果您不仅仅是从这里列出的内容中简单地复制,那么您可能需要在初始编码方面投入更多的工作;)
$lookup
不能正确替换超过一层深度的对象。虽然MongoDB 3.6可以以某种“荒谬”的方式做到这一点,但我个人不认为它很直观。通常的处理方法是使用$unwind
,然后使用$group
重新构建成数组。 - Neil LunnisFollow
键... 我能否使用 populate 发送该键值... 或者还有其他方法吗? - Ashh