按日期时间过滤子文档

Filter subdocument by datetime

我有以下型号

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 ]
});

我想创建两种方法来获取某个日期后由用户或 conversationId 生成的评论。

按用户

我试过下面的方法

var query = { 
    members : { $all : [ userId, otherUserId ], "$size" : 2 }
    , comments : { $elemMatch : { creationDate : { $gte: from } } } 
};

如果在指定日期(从)之后没有评论,方法 returns [] 或 null

通过会话 ID

当我尝试通过用户 ID 获取时,同样的情况发生了

var query = { _id : conversationId
    , comments : { $elemMatch : { creationDate : { $gte: from } } } 
};

有没有办法让方法returns的对话信息空评论?

谢谢!

这里听起来像是几个问题,但逐步解决了所有问题

为了从一个数组中得到多个匹配"or" none需要mapReduce的聚合框架来做到这一点。您可以尝试 "projecting" 与 $elemMatch 但这只能 return 与 "first" 匹配。即:

{ "a": [1,2,3] }

db.collection.find({ },{ "$elemMatch": { "$gte": 2 } })

{ "a": [2] }

因此标准投影不适用于此。它可以 return 一个 "empty" 数组,但它也只能 return 匹配的 "first"。

继续前进,您的代码中也有这个:

{ $all : [ userId, otherUserId ], "$site" : 2 }

其中 $site 不是有效的运算符。我认为你的意思是 $size 但实际上有 "two" 个运算符使用该名称,你的意图在这里可能不清楚。

如果您的意思是要测试的数组必须有 "only two" 个元素,那么这就是适合您的运算符。如果你的意思是两个人之间的匹配对话必须等于匹配中的两个人,那么 $all 无论如何都会这样做,所以 $size 在任何一种情况下都会变得多余,除非你不想要其他人在对话中。

关于聚合问题。您需要 "filter" 中的数组内容 "non-destructive way" 才能获得多个匹配项或一个空数组。

最好的方法是使用 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 which can process an expression against each array element. In this case the vallues are tested under the $cond 三进制来 return 条件为 true 的数组元素,否则 return false 作为元素。

然后 "filtered" 由 $setDifference 运算符进行 "filtered",该运算符本质上是将 $map 的结果数组与另一个数组 [false] 进行比较。这将从结果数组中删除任何 false 值,只留下匹配的元素或根本没有元素。

替代方法可能是 $redact,但由于您的文档在多个级别包含 "creationDate",因此这会扰乱其 $$DESCEND 运算符所使用的逻辑。这排除了该操作。

在早期版本中 "not destroying" 数组需要小心处理。所以你需要做很多相同的 "filter" 结果才能得到你想要的 "empty" 数组:

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 the content. When you $group 返回的数组内容,您查看每个元素以查看它是否符合条件以决定要 return 的内容,同时记录匹配项的数量。

这将把一些(一个带有 $addToSetfalse 结果放在数组中,或者只放在一个没有匹配项的 false 数组中。所以你用 $match 过滤掉这些,但也在匹配的 "count" 上进行测试,看看是否没有找到匹配项。如果未找到匹配项,则您不会丢弃该项目。

而是在最终 $project.

中将 [false] 数组替换为空数组

因此,根据您的 MongoDB 版本,这是要处理的 "fast/easy" 或 "slow/hard"。更新已经存在多年的版本的令人信服的理由。


工作示例

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" 仅包含最后两个条目,即 "greater than or equal" 到用作输入的日期(即第二条评论中的日期)。