【问题标题】:Filter subdocument by datetime按日期时间过滤子文档
【发布时间】:2015-07-15 18:13:13
【问题描述】:

我有以下型号

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在日期之后生成的cmets。

按用户

我尝试了以下方法

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

当在指定日期(at from)之后没有 cmets 时,该方法返回 [] 或 null

按对话ID

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

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

有什么办法可以让方法返回一个空的cmets的对话信息?

谢谢!

【问题讨论】:

    标签: mongodb mongoose aggregation-framework


    【解决方案1】:

    这听起来像是几个问题,但要逐步解决它们

    为了从数组中获得多个匹配“或”不匹配,需要 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”仅包含最后两个条目,它们“大于或等于”用作输入的日期(即来自第二条评论的日期)。

    【讨论】:

    • 嗨布莱克斯七,非常感谢您的解释。我有 Mongo 2.6.3,但是当我尝试应用您的第一个提案时,会引发下一个异常: {"name":"MongoError","errmsg":"exception: field contains is not allowed inside of $expressions","代码":16420,"ok":0}
    • @IvánPeralta 您输入的内容一定与此处列出的内容不同。我列出的所有内容都是有效的。
    • 我没有,只是通过对话改变了模型,错误是 {"name":"MongoError","errmsg":"exception: field contains is not allowed inside of $expressions", "code":16420,"ok":0} v2.6.3
    • @IvánPeralta 我在上面没有看到任何语法错误,这可能是唯一的原因。我会尝试写一个示例并运行它来证明它。
    • 谢谢 Blakes,顺便说一句:在我的集成测试中,当我使用 $elemMatch 时,我收到了整个 cmets 列表,而不仅仅是第一个。它按预期工作,问题是当没有 cmets 与标准匹配时。无论如何感谢布雷克斯
    猜你喜欢
    • 2013-10-20
    • 2020-07-21
    • 2021-09-01
    • 2021-12-27
    • 1970-01-01
    • 2018-01-25
    • 2015-07-18
    • 2019-10-09
    • 2023-03-23
    相关资源
    最近更新 更多