sql >> Base de Datos >  >> NoSQL >> MongoDB

MongoDB combina el recuento de elementos de colección relacionados con otros resultados de colección

De cualquier manera que mire esto, siempre que tenga una relación normalizada como esta, necesitará dos consultas para obtener un resultado que contenga detalles de la colección de "tareas" y complete con detalles de la colección de "proyectos". MongoDB no usa uniones de ninguna manera, y mongoose no es diferente. Mongoose ofrece .populate() , pero eso es solo magia de conveniencia para lo que esencialmente es ejecutar otra consulta y fusionar los resultados en el valor del campo al que se hace referencia.

Entonces, este es un caso en el que tal vez finalmente considere incorporar la información del proyecto en la tarea. Por supuesto que habrá duplicación, pero hace que los patrones de consulta sean mucho más simples con una colección singular.

Al mantener las colecciones separadas con un modelo de referencia, básicamente tiene dos enfoques. Pero primero puede usar agregar para obtener resultados más acordes con sus requisitos reales:

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

        }
    );

Esto simplemente usa un $group canalización para acumular en los valores de "projectid" dentro de la colección de "tareas". Para contar los valores de "completado" e "incompleto" usamos $cond operador que es un ternario para decidir qué valor pasar a $sum . Dado que la primera condición o "si" aquí es una evaluación booleana, entonces el campo booleano "completo" existente servirá, pasando donde true a "then" o "else" pasando el tercer argumento.

Esos resultados están bien, pero no contienen ninguna información de la colección "proyecto" para los valores "_id" recopilados. Un enfoque para hacer que la salida se vea de esta manera es llamar a la forma modelo de .populate() desde dentro de la devolución de llamada de resultados de agregación en el objeto de "resultados" devuelto:

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

De esta forma el .populate() call toma un objeto o matriz de datos como su primer argumento, siendo el segundo un documento de opciones para la población, donde el campo obligatorio aquí es para "ruta". Esto procesará todos los elementos y los "rellenará" a partir del modelo que se llamó insertando esos objetos en los datos de resultados en la devolución de llamada.

Como una lista de ejemplo completa:

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

Y esto dará resultados como estos:

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

Entonces .populate() funciona bien para este tipo de resultado de agregación, incluso para otra consulta, y generalmente debería ser adecuado para la mayoría de los propósitos. Sin embargo, hubo un ejemplo específico incluido en la lista donde hay "dos" proyectos creados pero, por supuesto, solo "una" tarea que hace referencia a solo uno de los proyectos.

Dado que la agregación está trabajando en la colección de "tareas", no tiene conocimiento alguno de ningún "proyecto" al que no se haga referencia allí. Para obtener una lista completa de "proyectos" con los totales calculados, debe ser más específico ejecutando dos consultas y "fusionando" los resultados.

Esto es básicamente una "combinación de hash" en claves y datos distintos, sin embargo, un buen ayudante para esto es un módulo llamado nedb , que le permite aplicar la lógica de una manera más coherente con las consultas y operaciones de MongoDB.

Básicamente, desea una copia de los datos de la colección de "proyectos" con campos aumentados, luego desea "fusionar" o .update() esa información con los resultados de la agregación. Nuevamente como una lista completa para demostrar:

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

Y los resultados aquí:

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

Eso enumera los datos de cada "proyecto" e incluye los valores calculados de la colección de "tareas" relacionada con él.

Entonces, hay algunos enfoques que puede hacer. Nuevamente, en última instancia, es mejor que incorpore "tareas" en los elementos del "proyecto", lo que nuevamente sería un enfoque de agregación simple. Y si va a incrustar la información de la tarea, también puede mantener contadores para "completo" e "incompleto" en el objeto "proyecto" y simplemente actualizarlos a medida que los elementos se marcan como completados en la matriz de tareas con el $inc operador.

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

Sin embargo, debe conocer el estado actual de la tarea para que las actualizaciones del contador funcionen correctamente, pero esto es fácil de codificar y probablemente debería tener al menos esos detalles en un objeto que pasa a sus métodos.

Personalmente, remodelaría a la última forma y haría eso. Puede hacer "fusiones" de consultas como se muestra en dos ejemplos aquí, pero, por supuesto, tiene un costo.