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



我是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 查询中是否可能。感谢任何帮助。谢谢!

无论您以何种方式看待这一点,只要您有这样的规范化关系,那么您就需要两个查询来获取包含"任务"集合中的详细信息并填写"项目"集合中的详细信息的结果。MongoDB不以任何方式使用连接,Mongoose也不例外。猫鼬确实提供了.populate(),但这只是本质上运行另一个查询并合并引用字段值的结果的便利魔术。

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

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

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

这仅使用$group管道来累积"任务"集合中的"projectid"值。为了计算"已完成"和"不完整"的值,我们使用三元$cond运算符来决定将哪个值传递给$sum。由于这里的第一个或"if"条件是布尔计算,那么现有的布尔"完整"字段就可以了,将true的位置传递给"then"或"else"传递第三个参数。

这些结果没问题,但它们不包含"项目"集合中收集的"_id"值的任何信息。使输出看起来像这样的一种方法是从返回的"results"对象的聚合结果回调中调用.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
);

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

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

当你需要在MongoDB中对事物进行分组或计数时,你通常需要使用聚合框架。以下是在外壳中计算数据的方法:

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

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

我从未使用过猫鼬,但现在您可以从:)开始

最新更新