MongoDB 将相关收集项计数与其他收集结果合并

Posted

技术标签:

【中文标题】MongoDB 将相关收集项计数与其他收集结果合并【英文标题】:MongoDB merge related collection item count with other collection results 【发布时间】:2015-03-18 22:23:39 【问题描述】:

我是 mongodb 的新手,正在尝试弄清楚如何有效地查询集合中的每个项目。

我有projects 收藏和tasks 收藏

//projects

   _id: ObjectId(),
   name: String

//tasks

   _id: ObjectId(),
   projectId: ObjectId(), //reference project id
   completed: Bool

我想获取所有项目,然后计算每个项目的completedincomplete 任务

db.projects.find()...
//perhaps something similar in output
[
 
   _id: ObjectId(), //projectId
   name: String
   completed: Number,
   incomplete: Number
 
]

我使用猫鼬作为 ORM。我不知道这在猫鼬甚至本机 mongodb 查询中是否可行。感谢任何帮助。谢谢!

【问题讨论】:

【参考方案1】:

无论您怎么看,只要您有这样的规范化关系,您就需要两个查询来获取包含“tasks”集合中的详细信息并填写“projects”集合中的详细信息的结果。 MongoDB 不以任何方式使用连接,mongoose 也不例外。 Mongoose 确实提供了.populate(),但这只是方便魔术,因为它本质上是运行另一个查询并将结果合并到引用的字段值上。

因此,在这种情况下,您最终可能会考虑将项目信息嵌入任务中。当然会有重复,但使用单一集合会使查询模式更加简单。

将集合与引用模型分开,您基本上有两种方法。但首先您可以使用aggregate 以获得更符合您实际需求的结果:

      Task.aggregate(
        [
           "$group": 
            "_id": "$projectId",
            "completed": 
              "$sum": 
                "$cond": [ "$completed", 1, 0 ]
              
            ,
            "incomplete": 
              "$sum": 
                "$cond": [ "$completed", 0, 1 ]
              
            
          
        ],
        function(err,results) 

        
    );

这仅使用$group 管道来累积“tasks”集合中“projectid”的值。为了计算“完成”和“未完成”的值,我们使用$cond 运算符,它是一个三元组来决定将哪个值传递给$sum。由于此处的第一个或“if”条件是布尔评估,因此现有的布尔“完整”字段将执行,将 where true 传递给“then”或“else”并传递第三个参数。

这些结果没有问题,但它们不包含来自“项目”集合中收集的“_id”值的任何信息。使输出看起来像这种方式的一种方法是从返回的“结果”对象的聚合结果回调中调用 .populate() 的模型形式:

    Project.populate(results, "path": "_id" ,callback);

在这种形式中,.populate() 调用将一个对象或数据数组作为它的第一个参数,第二个是人口的选项文档,这里的必填字段是“路径”。这将处理任何项目并从被调用的模型中“填充”,将这些对象插入回调中的结果数据中。

作为一个完整的示例清单:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema;

var projectSchema = new Schema(
  "name": String
);

var taskSchema = new Schema(
  "projectId":  "type": Schema.Types.ObjectId, "ref": "Project" ,
  "completed":  "type": Boolean, "default": false 
);

var Project = mongoose.model( "Project", projectSchema );
var Task = mongoose.model( "Task", taskSchema );

mongoose.connect('mongodb://localhost/test');

async.waterfall(
  [
    function(callback) 
      async.each([Project,Task],function(model,callback) 
        model.remove(,callback);
      ,
      function(err) 
        callback(err);
      );
    ,

    function(callback) 
      Project.create( "name": "Project1" ,callback);
    ,

    function(project,callback) 
      Project.create( "name": "Project2" ,callback);
    ,

    function(project,callback) 
      Task.create( "projectId": project ,callback);
    ,

    function(task,callback) 
      Task.aggregate(
        [
           "$group": 
            "_id": "$projectId",
            "completed": 
              "$sum": 
                "$cond": [ "$completed", 1, 0 ]
              
            ,
            "incomplete": 
              "$sum": 
                "$cond": [ "$completed", 0, 1 ]
              
            
          
        ],
        function(err,results) 
          if (err) callback(err);
          Project.populate(results, "path": "_id" ,callback);
        
      );
    
  ],
  function(err,results) 
    if (err) throw err;
    console.log( JSON.stringify( results, undefined, 4 ));
    process.exit();
  
);

这将产生如下结果:

[
    
        "_id": 
            "_id": "54beef3178ef08ca249b98ef",
            "name": "Project2",
            "__v": 0
        ,
        "completed": 0,
        "incomplete": 1
    
]

所以.populate() 适用于这种聚合结果,即使是有效的另一个查询,通常应该适用于大多数用途。然而,清单中包含一个特定示例,其中创建了“两个”项目,但当然只有“一个”任务仅引用其中一个项目。

由于聚合正在处理“任务”集合,因此它对任何未在其中引用的“项目”一无所知。为了获得包含计算总数的完整“项目”列表,您需要更具体地运行两个查询并“合并”结果。

这基本上是对不同键和数据的“哈希合并”,但是对此的好帮手是一个名为 nedb 的模块,它允许您以与 MongoDB 查询和操作更一致的方式应用逻辑。

基本上,您需要“项目”集合中的数据副本以及增强字段,然后您希望将该信息与聚合结果“合并”或.update()。再次作为一个完整的清单来演示:

var async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema,
    DataStore = require('nedb'),
    db = new DataStore();


var projectSchema = new Schema(
  "name": String
);

var taskSchema = new Schema(
  "projectId":  "type": Schema.Types.ObjectId, "ref": "Project" ,
  "completed":  "type": Boolean, "default": false 
);

var Project = mongoose.model( "Project", projectSchema );
var Task = mongoose.model( "Task", taskSchema );

mongoose.connect('mongodb://localhost/test');

async.waterfall(
  [
    function(callback) 
      async.each([Project,Task],function(model,callback) 
        model.remove(,callback);
      ,
      function(err) 
        callback(err);
      );
    ,

    function(callback) 
      Project.create( "name": "Project1" ,callback);
    ,

    function(project,callback) 
      Project.create( "name": "Project2" ,callback);
    ,

    function(project,callback) 
      Task.create( "projectId": project ,callback);
    ,

    function(task,callback) 
      async.series(
        [

          function(callback) 
            Project.find(,function(err,projects) 
              async.eachLimit(projects,10,function(project,callback) 
                db.insert(
                  "projectId": project._id.toString(),
                  "name": project.name,
                  "completed": 0,
                  "incomplete": 0
                ,callback);
              ,callback);
            );
          ,

          function(callback) 
            Task.aggregate(
              [
                 "$group": 
                  "_id": "$projectId",
                  "completed": 
                    "$sum": 
                      "$cond": [ "$completed", 1, 0 ]
                    
                  ,
                  "incomplete": 
                    "$sum": 
                      "$cond": [ "$completed", 0, 1 ]
                    
                  
                
              ],
              function(err,results) 
                async.eachLimit(results,10,function(result,callback) 
                  db.update(
                     "projectId": result._id.toString() ,
                     "$set": 
                        "complete": result.complete,
                        "incomplete": result.incomplete
                      
                    ,
                    callback
                  );
                ,callback);
              
            );
          ,

        ],

        function(err) 
          if (err) callback(err);
          db.find(, "_id": 0 ,callback);
        
      );
    
  ],
  function(err,results) 
    if (err) throw err;
    console.log( JSON.stringify( results, undefined, 4 ));
    process.exit();
  

这里的结果:

[
    
        "projectId": "54beef4c23d4e4e0246379db",
        "name": "Project2",
        "completed": 0,
        "incomplete": 1
    ,
    
        "projectId": "54beef4c23d4e4e0246379da",
        "name": "Project1",
        "completed": 0,
        "incomplete": 0
    
]

这列出了来自每个“项目”的数据,并包括来自与其相关的“任务”集合的计算值。

因此,您可以采取一些方法。同样,您最终可能最好将“任务”嵌入到“项目”项中,这又是一种简单的聚合方法。如果您要嵌入任务信息,那么您也可以在“项目”对象上维护“完成”和“未完成”计数器,并在任务数组中使用$inc 标记完成时简单地更新这些计数器运算符。

var taskSchema = new Schema(
  "completed":  "type": Boolean, "default": false 
);

var projectSchema = new Schema(
  "name": String,
  "completed":  "type": Number, "default": 0 ,
  "incomplete":  "type": Number, "default": 0 
  "tasks": [taskSchema]
);

var Project = mongoose.model( "Project", projectSchema );
// cheat for a model object with no collection
var Task = mongoose.model( "Task", taskSchema, undefined );

// Then in later code

// Adding a task
var task = new Task();
Project.update(
     "task._id":  "$ne": task._id  ,
     
        "$push":  "tasks": task ,
        "$inc": 
            "completed": ( task.completed ) ? 1 : 0,
            "incomplete": ( !task.completed ) ? 1 : 0;
        
    ,
    callback
 );

// Removing a task
Project.update(
     "task._id": task._id ,
     
        "$pull":  "tasks":  "_id": task._id  ,
        "$inc": 
            "completed": ( task.completed ) ? -1 : 0,
            "incomplete": ( !task.completed ) ? -1 : 0;
        
    ,
    callback
 );


 // Marking complete
Project.update(
     "tasks":  "$elemMatch":  "_id": task._id, "completed": false  ,
     
        "$set":  "tasks.$.completed": true ,
        "$inc": 
            "completed": 1,
            "incomplete": -1
        
    ,
    callback
);

您必须知道当前任务状态才能使计数器更新正常工作,但这很容易编写代码,您可能至少应该在传递给您的方法的对象中包含这些详细信息。

就我个人而言,我会重新建模为后一种形式并这样做。您可以执行查询“合并”,如此处的两个示例所示,但这当然是有代价的。

【讨论】:

谢谢你,这是一个很好的答案!是的,我考虑过项目中的嵌入式任务,但我担心我的项目文档可能会达到文档大小限制。 您始终可以$pull从数组中完成任务,或者至少通过一些逻辑对那里的总任务保持“上限”。也许$slice 或其他自定义逻辑只保留这么多“已完成”项目。取决于您真正想要做什么,如果数组中的项目少于 500 个,性能应该不会真正下降。【参考方案2】:

当您需要在 MongoDB 中对事物进行分组或计数时,您通常需要使用 aggregation framework。以下是在 shell 中计算数据的方法:

db.tasks.aggregate([ $group: 
  _id: projectID: "$projectID", completed: "$completed",
  count: $sum: 1
  );

这将为项目中的每个任务返回两个文档 - 一个包含已完成任务的计数,另一个包含尚未完成的任务。

我从来没有用过 Mongoose,但现在你有一些东西可以开始了:)

【讨论】:

所以,我必须运行两个查询,一个用于查找所有项目,另一个用于汇总计数并在客户端进行映射,对吧? 仅当您需要来自项目的一些额外数据时

以上是关于MongoDB 将相关收集项计数与其他收集结果合并的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB相关资料收集

Trello 如何在 MongoDB 中存储数据? (每板收集?)

使用刷新对象收集 wmi 性能计数器

使用PowerShell收集多台服务器的性能计数器

收集有关 Linux 上的线程调度的信息

JVM 垃圾收集器与内存分配策略