我有以下模型
var messageSchema = new Schema({
creationDate: { type: Date, default: Date.now },
comment: { type: String },
author: { type: Schema.Types.ObjectId }
});
var conversationSchema = new Schema({
title: { type: String },
author: { type : Schema.Types.ObjectId },
members: [ { type: Schema.Types.ObjectId } ],
creationDate: { type: Date, default: Date.now },
lastUpdate: { type: Date, default: Date.now },
comments: [ messageSchema ]
});
我想创建两个方法来获取user或conversationId在日期之后生成的评论。
由用户我尝试了以下方法
var query = {
members : { $all : [ userId, otherUserId ], "$size" : 2 }
, comments : { $elemMatch : { creationDate : { $gte: from } } }
};
当指定日期(at from)之后没有注释时,该方法返回[]或null
由conversationId当我尝试通过用户id
获取时也会发生同样的情况var query = { _id : conversationId
, comments : { $elemMatch : { creationDate : { $gte: from } } }
};
是否有任何方法可以使该方法返回带有空注释的对话信息?
谢谢!
听起来这里有几个问题,但是逐步解决它们
为了从数组中获得多个匹配"或"none需要mapReduce的聚合框架来完成此操作。您可以尝试使用 $elemMatch
"投影",但这只能返回"第一个"匹配。即:
{ "a": [1,2,3] }
db.collection.find({ },{ "$elemMatch": { "$gte": 2 } })
{ "a": [2] }
所以标准投影在这里不起作用。它可以返回一个"空"数组,但也只能返回匹配的"第一个"数组。
继续,在你的代码中也有:
{ $all : [ userId, otherUserId ], "$site" : 2 }
其中$site
不是一个有效的运算符。我想你的意思是$size
,但实际上有"两个"操作符与该名称,你的意图可能不清楚这里。
如果您的意思是要测试的数组必须"只有两个"元素,那么这就是适合您的操作符。如果你的意思是两个人之间匹配的对话必须等于匹配中的两个人,那么$all
无论如何都会这样做,所以$size
在任何情况下都是多余的,除非你不想让任何人参与对话。
关于聚合问题。您需要以"非破坏性方式"过滤"数组的内容,以便获得多个匹配项或空数组。
最好的方法是使用2.6版本提供的现代MongoDB功能,它允许在不处理$unwind
的情况下过滤数组内容:
Model.aggregate(
[
{ "$match": {
"members": { "$all": [userId,otherUserId] }
}},
{ "$project": {
"title": 1,
"author": 1,
"members": 1,
"creationDate": 1,
"lastUpdate": 1,
"comments": {
"$setDifference": [
{ "$map": {
"input": "$comments",
"as": "c",
"in": { "$cond": [
{ "$gte": [ "$$c.creationDate", from ] },
"$$c",
false
]}
}},
[false]
]
}
}}
],
function(err,result) {
}
);
使用 $map
,可以针对每个数组元素处理表达式。在这种情况下,在 $cond
三进制下测试值,返回条件为true
的数组元素,否则返回false
作为元素。
然后由 $setDifference
运算符"过滤",该运算符本质上是将$map
的结果数组与另一个数组[false]
进行比较。这将从结果数组中删除任何false
值,只留下匹配的元素或根本没有元素。
另一种可能是 $redact
,但由于您的文档在多个级别包含"creationDate",因此这会与$$DESCEND
操作符使用的逻辑混淆。
在早期版本中,"不销毁"数组需要小心处理。因此,您需要对结果进行相同的"筛选",以获得您想要的"空"数组:
Model.aggregate(
[
{ "$match": {
"$and": [
{ "members": userId },
{ "members": otherUserId }
}},
{ "$unwind": "$comments" },
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"author": { "$first": "$author" },
"members": { "$first": "$members" },
"creationDate": { "$first": "$creationDate" },
"lastUpdate": { "$first": "$lastUpdate" },
"comments": {
"$addToSet": {
"$cond": [
{ "$gte": [ "$comments.creationDate", from ] },
"$comments",
false
]
}
},
"matchedSize": {
"$sum": {
"$cond": [
{ "$gte": [ "$comments.creationDate", from ] },
1,
0
]
}
}
}},
{ "$unwind": "$comments" },
{ "$match": {
"$or": [
{ "comments": { "$ne": false } },
{ "matchedSize": 0 }
]
}},
{ "$group": {
"_id": "$_id",
"title": { "$first": "$title" },
"author": { "$first": "$author" },
"members": { "$first": "$members" },
"creationDate": { "$first": "$creationDate" },
"lastUpdate": { "$first": "$lastUpdate" },
"comments": { "$push": "$comments" }
}},
{ "$project": {
"title": 1,
"author": 1,
"members": 1,
"creationDate": 1,
"lastUpdate": 1,
"comments": {
"$cond": [
{ "$eq": [ "$comments", [false] ] },
{ "$const": [] },
"$comments"
]
}
}}
],
function(err,result) {
}
)
做了很多相同的事情,但时间更长。为了查看数组内容,您需要 $unwind
内容。当您返回 $group
时,您将查看每个元素,看它是否符合决定返回内容的条件,并记录匹配的计数。
这将把一些($addToSet
) false
结果放在数组中,或者只放在一个没有匹配的条目false
的数组中。所以你用 $match
过滤掉这些,但也测试匹配的"计数",看看是否没有找到匹配。如果没有找到匹配项,则不要丢弃该物品。
在final $project
中将[false]
数组替换为空数组。
所以根据你的MongoDB版本,这是"快速/容易"或"慢/难"处理。更新已有多年历史的版本的令人信服的理由。
工作示例
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/aggtest');
var memberSchema = new Schema({
name: { type: String }
});
var messageSchema = new Schema({
creationDate: { type: Date, default: Date.now },
comment: { type: String },
});
var conversationSchema = new Schema({
members: [ { type: Schema.Types.ObjectId } ],
comments: [messageSchema]
});
var Member = mongoose.model( 'Member', memberSchema );
var Conversation = mongoose.model( 'Conversation', conversationSchema );
async.waterfall(
[
// Clean
function(callback) {
async.each([Member,Conversation],function(model,callback) {
model.remove({},callback);
},
function(err) {
callback(err);
});
},
// add some people
function(callback) {
async.map(["bill","ted","fred"],function(name,callback) {
Member.create({ "name": name },callback);
},callback);
},
// Create a conversation
function(names,callback) {
var conv = new Conversation();
names.forEach(function(el) {
conv.members.push(el._id);
});
conv.save(function(err,conv) {
callback(err,conv,names)
});
},
// add some comments
function(conv,names,callback) {
async.eachSeries(names,function(name,callback) {
Conversation.update(
{ "_id": conv._id },
{ "$push": { "comments": { "comment": name.name } } },
callback
);
},function(err) {
callback(err,names);
});
},
function(names,callback) {
Conversation.findOne({},function(err,conv) {
callback(err,names,conv.comments[1].creationDate);
});
},
function(names,from,callback) {
var ids = names.map(function(el) {
return el._id
});
var pipeline = [
{ "$match": {
"$and": [
{ "members": ids[0] },
{ "members": ids[1] }
]
}},
{ "$project": {
"members": 1,
"comments": {
"$setDifference": [
{ "$map": {
"input": "$comments",
"as": "c",
"in": { "$cond": [
{ "$gte": [ "$$c.creationDate", from ] },
"$$c",
false
]}
}},
[false]
]
}
}}
];
//console.log(JSON.stringify(pipeline, undefined, 2 ));
Conversation.aggregate(
pipeline,
function(err,result) {
if(err) throw err;
console.log(JSON.stringify(result, undefined, 2 ));
callback(err);
}
)
}
],
function(err) {
if (err) throw err;
process.exit();
}
);
产生如下输出:
[
{
"_id": "55a63133dcbf671918b51a93",
"comments": [
{
"comment": "ted",
"_id": "55a63133dcbf671918b51a95",
"creationDate": "2015-07-15T10:08:51.217Z"
},
{
"comment": "fred",
"_id": "55a63133dcbf671918b51a96",
"creationDate": "2015-07-15T10:08:51.220Z"
}
],
"members": [
"55a63133dcbf671918b51a90",
"55a63133dcbf671918b51a91",
"55a63133dcbf671918b51a92"
]
}
]
注意"comments"只包含最后两个大于或等于作为输入的日期(即第二个注释的日期)的条目。