我们说我有两个Mongoose集合 - conversation
和message
- 我希望显示特定用户所在的对话列表,按最新消息排序,在会话名称下方预览该消息
在我收到用户的对话后,如何才能选择每个对话的最新消息,并将这些消息附加到相应的对话中? (鉴于架构看起来像这样):
var ConversationSchema = new Schema({
name: String,
participants: {
type: [{
type: Schema.Types.ObjectId,
ref: 'User'
}]
}
});
var MessageSchema = new Schema({
conversation: {type: Schema.Types.ObjectId, ref: 'Conversation', required: true},
text: {type: String, required: true},
user: {type: Schema.Types.ObjectId, ref: 'User', required: true}
});
我收到的消息是我应该使用Mongo"聚合"框架,但我以前从未使用过,所以我想要一些帮助。谢谢!
答案 0 :(得分:5)
聚合框架确实将成为您完成此任务的伙伴。通过聚合,它可以通过使用过滤器,分组器,分类器,转换和其他操作员的多阶段管道将集合提取到基本信息。与其他技术相比,这组蒸馏结果的生成效率更高。
对于您的上述用例,聚合包含一系列应用于称为管道的集合的特殊运算符:
[
{ "$match": { /* options */ } },
{ "$lookup": { /* options */ } },
{ "$group": { /* options */ } }
]
执行管道时,MongoDB将运营商互相管道。 "管"这里采用Linux的含义:运算符的输出成为以下运算符的输入。每个运算符的结果是一个新的文档集合。所以Mongo按如下方式执行上一个管道:
collection | $match | $lookup | $group => result
现在,将上述内容应用到您的任务中,您需要 $match
管道步骤作为聚合的第一个阶段,因为它允许您仅使用匹配的文档过滤文档流未经修改进入下一个管道阶段。因此,如果您只使用Conversation
模型运行聚合
$match
查询对象,您将获得特定用户所在的文档:
Conversation.aggregate([
{ "$match": { "participants.type": userId } }
]).exec(function(err, result){
if (err) throw err;
console.log(result);
})
您可以管道其他运算符,在这种情况下,您需要 $lookup
运算符,该运算符对同一数据库中的messages
集合执行左外连接以过滤文档来自"加入"处理集合:
Conversation.aggregate([
{ "$match": { "participants.type": userId } },
{
"$lookup": {
"from": "messages",
"localfield": "_id",
"foreignField": "conversation",
"as": "messages"
}
}
]).exec(function(err, result){
if (err) throw err;
console.log(result);
})
这将输出来控制conversation
文档中的字段以及名为" message"的新数组字段。其元素是来自"加入"的匹配文件。 messages
集合。 $lookup
阶段会将这些重新整理的文档传递到下一个阶段。
既然您已将conversations
与messages
一起提供给用户,那么您如何从每个对话中选择ONLY
最新消息?
这可以通过 $group
管道运算符实现,但在将 $group
运算符应用于上一个管道的文档之前,
您需要展平messages
数组以获取最新文档, $unwind
运算符将为您解构数组元素。
当 $unwind
运算符应用于数组字段时,它将为列表中的每个元素生成一条新记录, $unwind
< / strong>已应用:
Conversation.aggregate([
{ "$match": { "participants.type": userId } },
{
"$lookup": {
"from": "messages",
"localfield": "_id",
"foreignField": "conversation",
"as": "messages"
}
},
{ "$unwind": "$messages" }
]).exec(function(err, result){
if (err) throw err;
console.log(result);
})
假设您的MessageSchema
有一个时间戳字段(比如createdAt
)表示邮件发送的日期时间,您可以按该字段按降序重新排序文档,以便处理到下一个管道。 $sort
运算符非常适合:
Conversation.aggregate([
{ "$match": { "participants.type": userId } },
{
"$lookup": {
"from": "messages",
"localfield": "_id",
"foreignField": "conversation",
"as": "messages"
}
},
{ "$unwind": "$messages" },
{ "$sort": { "messages.createdAt": -1 } }
]).exec(function(err, result){
if (err) throw err;
console.log(result);
})
使用非规范化文档,您可以将数据分组以处理它们。组管道运算符类似于SQL的GROUP BY
子句。在SQL中,除非使用任何聚合函数(称为accumulators),否则不能使用GROUP BY
。同样,您也必须在MongoDB中使用聚合函数。 MongoDB仅使用_id
字段标识分组表达式,在这种情况下,按_id
字段对文档进行分组:
Conversation.aggregate([
{ "$match": { "participants.type": userId } },
{
"$lookup": {
"from": "messages",
"localfield": "_id",
"foreignField": "conversation",
"as": "messages"
}
},
{ "$unwind": "$messages" },
{ "$sort": { "messages.createdAt": -1 } }
{
"$group": {
"_id": "$_id",
"name": { "$first": "$name" },
"participants": { "$first": "$participants" },
"latestMessage": { "$first": "$message" }
}
}
]).exec(function(err, result){
if (err) throw err;
console.log(result);
})
在上文中,我们对 $first
组累加器运算符感兴趣,因为它从每个组的第一个文档返回一个值。由于上一个管道运算符按降序对文档进行排序,因此 $first
会为您提供LATEST
消息。
因此,运行上面的最后一个聚合操作将为您提供所需的结果。这假设MessageSchema
有一个时间戳,这对于确定最新消息至关重要,否则它只能用于 $unwind
步骤。