Node.js $lookup多个级别而不使用$unwind?
我有以下收藏:Node.js $lookup多个级别而不使用$unwind?,node.js,mongodb,mongoose,mongodb-query,aggregation-framework,Node.js,Mongodb,Mongoose,Mongodb Query,Aggregation Framework,我有以下收藏: 场地收藏 评论集 评论集 作者集 现在,下面的填充查询工作正常 const venues = await Venue.findOne({ _id: id.id }) .populate({ path: 'reviews', options: { sort: { createdAt: -1 } }, populate: [ { path: 'author' }, { path:
- 场地收藏
- 评论集
- 评论集
- 作者集
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”时,它会分割场地。。。我想在相同的数组(如填充)和相同的顺序审查
我想用$lookup
实现以下查询,因为作者有followers字段,所以我需要通过执行$project
发送字段isFollow
,这是使用填充
无法完成的
$project: {
isFollow: { $in: [mongoose.Types.ObjectId(req.user.id), '$followers'] }
}
当然,有几种方法取决于您可用的MongoDB版本。这些方法的用途各不相同,从使用到通过在结果上启用对象操作 我确实要求您仔细阅读这些章节,并意识到在考虑您的实现解决方案时,可能并非所有内容都是如此 MongoDB 3.6,“嵌套”$lookup 使用MongoDB 3.6,操作员可以添加一个
管道
表达式,而不是简单地将“本地”键值连接到“外部”键值,这意味着您可以在这些管道表达式中“嵌套”每个键值
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”
数组条目中看到的“展平author属性”。所有目标输出可能是一个“数组”,但在“子管道”中,我们可以将单个元素数组重新塑造为一个值
标准MongoDB$查找
仍然保留“服务器上的连接”,实际上您可以使用它,但它只需要中间处理。这是一种长期存在的解构阵列的方法,使用阶段和阶段来重建阵列:
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"
}
}
}}
])
这并不像您一开始想象的那样令人畏惧,而是遵循一个简单的和模式,随着您在每个阵列中的前进
“author”
详细信息当然是单数的,因此一旦“解开”了,您只需将其保持为单数,添加字段并开始“回滚”到数组中的过程
只有两个级别可以重建回原始的场馆
文档,因此第一个详细级别是通过查看
来重建评论
数组。要收集这些评论,您只需访问“$reviews.comments”
的路径,只要“$reviews.id”
字段位于“分组id”中,您只需保留所有其他字段。您也可以将所有这些内容放入\u id
中,也可以使用
完成后,只有一个阶段才能回到场馆
本身。这一次分组键当然是“$\u id”
,场馆本身的所有属性都在使用,剩余的“$review”
详细信息将返回到一个数组中。当然,前面的“$comments”
输出将成为“review.comments”
路径
在处理单个文档及其关系时,这并没有那么糟糕。管道操作符通常可能是一个性能问题,但在这种使用情况下,它不应该真正造成那么大的影响
由于数据仍在“连接到服务器上”,因此通信量仍远低于其他剩余的备选方案
JavaScript操作
当然,这里的另一种情况是,不是更改服务器本身的数据,而是实际操作结果。在大多数情况下,我会支持这种方法,因为对数据的任何“添加”可能最好在客户机上处理
当然,使用的问题是,虽然它看起来“像”一个简化得多的过程,但实际上它在任何方面都不是一个连接。实际上所做的只是“隐藏”向数据库提交多个查询的底层过程,然后通过异步处理等待结果
因此,连接的“外观”实际上是对服务器的多个请求的结果,然后对数据进行“客户端操作”,以将细节嵌入阵列中
因此,除了“强”>“清晰的警告< /强”之外,性能特征与服务器不太接近,另一个警告当然是结果中的“MangoOffice文档”实际上不是纯JavaScript对象,需要进一步操作。 因此,为了采用这种方法,您需要在执行之前将该方法添加到查询中,以便指示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.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
}
})
)
})
);
因此,这实际上只是一个循环遍历每个内部数组的问题,直到您可以在作者
详细信息中看到追随者
数组的级别。然后可以与存储在该数组a中的ObjectId
值进行比较
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"
}},
])
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"
}
}
}}
])
let venue = await Venue.findOne({ _id: id.id })
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 } },
populate: [
{ path: 'comments', populate: [{ path: 'author' }] }
]
})
.lean();
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
}
})
)
})
);