这是非常有可能的一个请求,这与MongoDB的基本工具是$lookup
。
我认为这实际上更有意义的从Song
集合中进行查询,因为您的标准是它们必须在该集合的两个属性之一中列出。 -
最优内部加入反
。假定实际的“模型”名字是什么上面列出:
var today = new Date.now(),
oneDay = 1000 * 60 * 60 * 24,
twoWeeksAgo = new Date(today - (oneDay * 14));
var userIds; // Should be assigned as an 'Array`, even if only one
Song.aggregate([
{ "$match": {
"$or": [
{ "author": { "$in": userIds } },
{ "collaborators": { "$in": userIds } }
],
"publishedDate": { "$gt": twoWeeksAgo }
}},
{ "$addFields": {
"users": {
"$setIntersection": [
userIds,
{ "$setUnion": [ ["$author"], "$collaborators" ] }
]
}
}},
{ "$lookup": {
"from": User.collection.name,
"localField": "users",
"foreignField": "_id",
"as": "users"
}},
{ "$unwind": "$users" },
{ "$group": {
"_id": "$users._id",
"email": { "$first": "$users.email" },
"name": { "$first": "$users.name" },
"gender": { "$first": "$users.gender" },
"songs": {
"$push": {
"_id": "$_id",
"track": "$track",
"author": "$author",
"url": "$url",
"title": "$title",
"photo": "$photo",
"publishedDate": "$publishedDate",
"views": "$views",
"likes": "$likes",
"collaborators": "$collaborators"
}
}
}}
])
这对我来说是最合乎逻辑的过程,只要它是一个“INNER JOIN”你想从结果,这意味着“所有用户必须提到至少有一首歌”在涉及的两个属性。
$setUnion
将“唯一列表”(ObjectId
是唯一的)组合这两个。所以如果一个“作者”也是一个“合作者”,那么他们只会列出一首歌。
$setIntersection
将列表从该组合列表“过滤”到仅在查询条件中指定的列表。这将删除所有不在选择中的其他“协作者”条目。
$lookup
对该组合数据进行“加入”以获取用户,并且$unwind
已完成,因为您希望User
成为主要细节。所以我们基本上将“用户数组”转换为结果中的“歌曲数组”。
此外,由于主要标准来自Song
,因此从该集合中查询作为方向是有意义的。
可选LEFT JOIN
各地这样做的另一种方式是在“左连接”被通缉,是“所有用户”,无论是否有任何相关的歌曲或不:
User.aggregate([
{ "$lookup": {
"from": Song.collection.name,
"localField": "_id",
"foreignField": "author",
"as": "authors"
}},
{ "$lookup": {
"from": Song.collection.name,
"localField": "_id",
"foreignField": "collaborators",
"as": "collaborators"
}},
{ "$project": {
"email": 1,
"name": 1,
"gender": 1,
"songs": { "$setUnion": [ "$authors", "$collaborators" ] }
}}
])
因此,声明的列表“看上去”更短,但它为了获得可能的“作者”和“合作者”的结果而不是一个结果,迫使“两个”阶段$lookup
阶段。所以实际的“加入”操作在执行时间上可能会很昂贵。
其余的很简单,应用相同的$setUnion
,但这次是“结果数组”,而不是数据的原始来源。
如果你想类似的“查询”条件和上面的“过滤器”,为“歌”而不是实际User
文件返回,则LEFT JOIN你居然$filter
数组内容“后” $lookup
:
User.aggregate([
{ "$lookup": {
"from": Song.collection.name,
"localField": "_id",
"foreignField": "author",
"as": "authors"
}},
{ "$lookup": {
"from": Song.collection.name,
"localField": "_id",
"foreignField": "collaborators",
"as": "collaborators"
}},
{ "$project": {
"email": 1,
"name": 1,
"gender": 1,
"songs": {
"$filter": {
"input": { "$setUnion": [ "$authors", "$collaborators" ] },
"as": "s",
"cond": {
"$and": [
{ "$setIsSubset": [
userIds
{ "$setUnion": [ ["$$s.author"], "$$s.collaborators" ] }
]},
{ "$gte": [ "$$s.publishedDate", oneWeekAgo ] }
]
}
}
}
}}
])
这意味着通过LEFT JOIN条件,返回所有User
文档,但仅包含任何“歌曲”的文档将符合“过滤条件”作为所提供的userIds
的一部分的文档。即使是列表中包含的用户,也只会显示publishedDate
所需范围内的“歌曲”。
$filter
中的主要添加是$setIsSubset
运算符,它是将userIds
中提供的列表与文档中存在的两个字段中的“组合”列表进行比较的简短方法。注意到由于每个$lookup
的早期条件,“当前用户”已经必须是“相关的”。
MongoDB的3.6预览
一个新的“子流水线”语法可$lookup
从MongoDB的3.6版本意味着,而非如图所示为LEFT JOIN变种“两节” $lookup
阶段,你可以在其实这个结构作为“子流水线”,这也优化之前返回结果的过滤内容:
User.aggregate([
{ "$lookup": {
"from": Song.collection.name,
"let": {
"user": "$_id"
},
"pipeline": [
{ "$match": {
"$or": [
{ "author": { "$in": userIds } },
{ "collaborators": { "$in": userIds } }
],
"publishedDate": { "$gt": twoWeeksAgo },
"$expr": {
"$or": [
{ "$eq": [ "$$user", "$author" ] },
{ "$setIsSubset": [ ["$$user"], "$collaborators" ]
]
}
}}
],
"as": "songs"
}}
])
,这是所有有它在这种情况下,由于$expr
允许的使用在"let"
中声明的变量与歌曲集合中的每个条目进行比较,以仅选择那些与其他查询条件相匹配的条目。结果只是那些匹配的歌曲,每个用户或一个空数组。因此,使整个“子流水线”只是一个$match
表达式,这与附加逻辑几乎相同,而不是固定的本地和外键。
所以你甚至可以在$lookup
之后的管道中添加一个阶段来过滤掉任何“空”数组结果,从而使整个结果成为INNER加入。
因此,我个人认为可以采取第一种方法,只使用第二种方法。
注:这里有几个选择并不真正适用。第一种是$lookup + $unwind + $match
coalescence的特例,其中虽然基本情况适用于最初的INNER Join示例,但它不能用于LEFT Join Case。
这是因为,为了使LEFT JOIN来获得的$unwind
的使用必须与preserveNullAndEmptyArrays: true
实现,这打破了该应用程序的规则unwinding
和matching
不能内的“卷起” $lookup
并应用于“返回结果之前”的国外集合。
因此,为什么它不在样本中应用,我们在返回的数组上使用$filter
,因为没有可用于返回结果的“之前”的外部集合的最佳操作,并且没有任何操作会停止所有结果为只匹配外键的歌曲返回。 INNER加入当然是不同的。另一种情况是.populate()
与猫鼬。最重要的区别是.populate()
不是单个请求,但只是一个编程“实际上发出多个查询的简写”。所以无论如何,实际上会发出多个查询,并且总是需要所有结果才能应用任何过滤。
这导致了实际应用过滤的限制,通常意味着当您使用需要应用于外部收集的条件的“客户端连接”时,您无法真正实现“分页”概念。
关于Querying after populate in Mongoose的更多详细信息,以及实际演示了如何将基本功能作为自定义方法连接到猫鼬模式中,但实际上使用下面的流水线处理$lookup
。
非常感谢,非常感谢。 –