Ash*_*shh 8 mongoose mongodb node.js mongodb-query aggregation-framework
我有以下收藏
场地集合
{
"_id" : ObjectId("5acdb8f65ea63a27c1facf86"),
"name" : "ASA College - Manhattan Campus",
"addedBy" : ObjectId("5ac8ba3582c2345af70d4658"),
"reviews" : [
ObjectId("5acdb8f65ea63a27c1facf8b"),
ObjectId("5ad8288ccdd9241781dce698")
]
}
Run Code Online (Sandbox Code Playgroud)
评论集合
{
"_id" : ObjectId("5acdb8f65ea63a27c1facf8b"),
"createdAt" : ISODate("2018-04-07T12:31:49.503Z"),
"venue" : ObjectId("5acdb8f65ea63a27c1facf86"),
"author" : ObjectId("5ac8ba3582c2345af70d4658"),
"content" : "nice place",
"comments" : [
ObjectId("5ad87113882d445c5cbc92c8")
],
}
Run Code Online (Sandbox Code Playgroud)
评论收集
{
"_id" : ObjectId("5ad87113882d445c5cbc92c8"),
"author" : ObjectId("5ac8ba3582c2345af70d4658"),
"comment" : "dcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsfdcfdsf",
"review" : ObjectId("5acdb8f65ea63a27c1facf8b"),
"__v" : 0
}
Run Code Online (Sandbox Code Playgroud)
作者集合
{
"_id" : ObjectId("5ac8ba3582c2345af70d4658"),
"firstName" : "Bruce",
"lastName" : "Wayne",
"email" : "bruce@linkites.com",
"followers" : [ObjectId("5ac8b91482c2345af70d4650")]
}
Run Code Online (Sandbox Code Playgroud)
现在我的以下填充查询工作正常
const venues = await Venue.findOne({ _id: id.id })
.populate({
path: 'reviews',
options: { sort: { createdAt: -1 } },
populate: [
{ path: 'author' },
{ path: 'comments', populate: [{ path: 'author' }] }
]
})
Run Code Online (Sandbox Code Playgroud)
但我希望通过$lookup查询来实现它,但是当我对评论进行'$ unwind'时,它会拆分场地...我希望在同一个数组中进行评论(比如填充)和相同的顺序......
我希望实现以下查询,$lookup因为作者有关注者字段所以我需要isFollow通过执行发送字段,$project这是无法使用populate...
$project: {
isFollow: { $in: [mongoose.Types.ObjectId(req.user.id), '$followers'] }
}
Run Code Online (Sandbox Code Playgroud)
Nei*_*unn 20
当然有几种方法取决于您可用的MongoDB版本.这些从不同的用法$lookup到对.populate()结果的启用对象操作都有所不同.lean().
我确实要求您仔细阅读这些部分,并注意在考虑实施解决方案时可能看起来不一样.
使用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"
}},
])
Run Code Online (Sandbox Code Playgroud)
这可能非常强大,正如您从原始管道的角度看到的,它实际上只知道向"reviews"数组添加内容,然后每个后续的"嵌套"管道表达式也只能看到它是来自连接的"内部"元素.
它功能强大,在某些方面它可能会更清晰,因为所有字段路径都相对于嵌套级别,但它确实启动了BSON结构中的缩进蠕变,并且您需要知道是否与数组匹配或遍历结构的奇异值.
注意我们也可以在这里做一些事情,比如"展平作者属性",如"comments"数组条目中所示.所有$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"
}
}
}}
])
Run Code Online (Sandbox Code Playgroud)
你可能会认为,在第一和遵循一个简单的模式这个真没有令人畏惧$lookup和$unwind你通过每个阵列进步.
"author"当然,细节是单一的,所以一旦它"解开",你只需要保持这种方式,使字段添加并开始"回滚"到数组中的过程.
只有两个级别可以重建回原始Venue文档,因此第一个详细级别是Review重建"comments"数组.您需要的只是收集这些内容$push的路径,"$reviews.comments"只要该"$reviews._id"字段位于"分组_id"中,您需要保留的唯一其他内容是所有其他字段.你也可以把所有这些都放进去_id,或者你可以使用$first.
完成后,只有一个$group阶段才能回归Venue自身.这次分组键"$_id"当然是场地本身的所有属性使用$first,剩下的"$review"细节返回到数组中$push.当然"$comments",前一个输出$group成为"review.comments"路径.
处理单个文档及其关系,这并不是真的那么糟糕.该$unwind管道运营商可以一般是一个性能问题,但在这使用的情况下,应该不是真的会导致多大的冲击.
由于数据仍然"在服务器上加入",因此仍然比其他剩余备选方案少得多.
当然,这里的另一种情况是,您实际上操纵结果,而不是更改服务器本身的数据.在大多数情况下,我会支持这种方法,因为对数据的任何"添加"可能最好在客户端上处理.
当然使用的问题populate()是虽然它可能"看起来像"一个更简化的过程,但事实上它并不是一种加入.所有populate()实际上都是"隐藏"向数据库提交多个查询的基础过程,然后通过异步处理等待结果.
因此,连接的"外观"实际上是对服务器的多个请求的结果,然后对数据进行"客户端操作"以将细节嵌入到数组中.
因此,除了明确警告性能特征与服务器无法接近之外$lookup,另一个警告当然是结果中的"mongoose Documents"实际上不是普通的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();
Run Code Online (Sandbox Code Playgroud)
现在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
}
})
)
})
);
Run Code Online (Sandbox Code Playgroud)
因此,只需循环遍历每个内部数组,直到可以followers在author细节中看到数组的级别.然后可以ObjectId在第一次使用之后对照存储在该数组中的.map()值进行比较,以便返回"string"值以进行比较,req.user.id这也是一个字符串(如果不是,那么也加上该字符串.toString()),因为它更容易一般来说,通过JavaScript代码以这种方式比较这些值.
虽然我需要强调它"看起来很简单",但它实际上是你真正想要避免的系统性能,因为那些额外的查询以及服务器和客户端之间的转移在处理时花了很多钱甚至由于请求开销,这增加了托管提供商之间的传输的实际成本.
这些基本上是你可以采取的方法,而不是"滚动你自己",你实际上对数据库执行"多个查询"而不是使用帮助.populate()程序.
使用填充输出,您可以像处理任何其他数据结构一样简单地操作结果中的数据,只要您应用于.lean()查询以转换或以其他方式从返回的mongoose文档中提取普通对象数据.
虽然聚合方法看起来更复杂,但在服务器上执行此工作有"更多"优势.可以对较大的结果集进行排序,可以进行计算以进一步过滤,当然,您可以对服务器的"单个请求"进行"单一响应",所有这些都没有额外的开销.
完全有争议的是,管道本身可以简单地基于已经存储在模式中的属性来构造.因此,编写自己的方法来基于附加的模式执行此"构造"应该不会太困难.
从长远来看,当然$lookup是更好的解决方案,但你可能需要在初始编码中加入更多的工作,如果你不仅仅是简单地从这里列出的内容进行复制;)
| 归档时间: |
|
| 查看次数: |
2716 次 |
| 最近记录: |