MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

MongoDB unisce il conteggio degli elementi della raccolta correlati con altri risultati della raccolta

Qualunque sia il modo in cui lo guardi, fintanto che hai una relazione normalizzata come questa, allora avresti bisogno di due query per ottenere un risultato contenente i dettagli dalla raccolta "attività" e la compilazione con i dettagli dalla raccolta "progetti". MongoDB non usa i join in alcun modo e mongoose non è diverso. Mongoose offre .populate() , ma questa è solo una magia di convenienza per ciò che essenzialmente sta eseguendo un'altra query e unendo i risultati sul valore del campo di riferimento.

Quindi questo è un caso in cui forse alla fine potresti considerare di incorporare le informazioni sul progetto nell'attività. Ovviamente ci saranno duplicazioni, ma rende i modelli di query molto più semplici con una raccolta singolare.

Mantenendo le raccolte separate con un modello di riferimento, hai sostanzialmente due approcci. Ma prima puoi usare aggregate per ottenere risultati più in linea con le tue effettive esigenze:

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

        }
    );

Questo utilizza semplicemente un $group pipeline per accumulare sui valori di "projectid" all'interno della raccolta "compiti". Per contare i valori per "completato" e "incompleto" utilizziamo $cond operatore che è un ternario per decidere quale valore passare a $sum . Poiché la prima condizione o "se" qui è una valutazione booleana, il campo booleano "completo" esistente funzionerà, passando dove true a "allora" o "altro" passando il terzo argomento.

Questi risultati vanno bene ma non contengono alcuna informazione dalla raccolta "progetto" per i valori "_id" raccolti. Un approccio per rendere l'output in questo modo è chiamare il modulo del modello di .populate() dall'interno del callback dei risultati dell'aggregazione sull'oggetto "results" restituito:

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

In questa forma il .populate() call accetta un oggetto o una matrice di dati come primo argomento, mentre il secondo è un documento di opzioni per la popolazione, dove il campo obbligatorio qui è per "percorso". Questo elaborerà tutti gli elementi e "popolerà" dal modello che è stato chiamato inserendo quegli oggetti nei dati dei risultati nel callback.

Come elenco di esempio completo:

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

E questo darà risultati come questo:

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

Quindi .populate() funziona bene per questo tipo di risultato di aggregazione, anche come efficacemente un'altra query, e dovrebbe generalmente essere adatto per la maggior parte degli scopi. C'era tuttavia un esempio specifico incluso nell'elenco in cui ci sono "due" progetti creati ma ovviamente solo "un" attività che fa riferimento a uno solo dei progetti.

Poiché l'aggregazione sta lavorando sulla raccolta "compiti", non ha alcuna conoscenza di alcun "progetto" che non sia ivi referenziato. Per ottenere un elenco completo di "progetti" con i totali calcolati è necessario essere più specifici nell'esecuzione di due query e nell'"unione" dei risultati.

Questa è fondamentalmente una "unione hash" su chiavi e dati distinti, tuttavia un valido aiuto per questo è un modulo chiamato nedb , che consente di applicare la logica in modo più coerente con le query e le operazioni di MongoDB.

Fondamentalmente vuoi una copia dei dati dalla raccolta "progetti" con campi aumentati, quindi vuoi "unire" o .update() tali informazioni con i risultati dell'aggregazione. Ancora una volta come elenco completo per dimostrare:

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

E i risultati qui:

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

Ciò elenca i dati di ogni "progetto" e include i valori calcolati dalla raccolta "attività" ad esso correlata.

Quindi ci sono alcuni approcci che puoi fare. Ancora una volta, alla fine potresti essere meglio semplicemente incorporare "attività" negli elementi del "progetto", che sarebbe ancora una volta un semplice approccio di aggregazione. E se intendi incorporare le informazioni sull'attività, puoi anche mantenere i contatori per "completato" e "incompleto" sull'oggetto "progetto" e semplicemente aggiornarli quando gli elementi sono contrassegnati come completati nell'array delle attività con il $inc operatore.

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

Tuttavia, devi conoscere lo stato dell'attività corrente affinché gli aggiornamenti del contatore funzionino correttamente, ma questo è facile da codificare e probabilmente dovresti avere almeno quei dettagli in un oggetto che passa nei tuoi metodi.

Personalmente vorrei rimodellare in quest'ultima forma e farlo. Puoi eseguire query di "unione" come mostrato in due esempi qui, ma ovviamente ha un costo.