这听起来像是几个问题,但要逐步解决它们
为了从数组中获得多个匹配“或”不匹配,需要 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 过滤掉这些,但也要测试匹配的“计数”,看看是否没有找到匹配项。如果没有找到匹配项,则不要丢弃该项目。
相反,您将 [false] 数组替换为最终 $project 中的空数组。
因此,根据您的 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"
]
}
]
请注意,“cmets”仅包含最后两个条目,它们“大于或等于”用作输入的日期(即来自第二条评论的日期)。