MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

MongoDB führt die Anzahl der zugehörigen Sammlungselemente mit anderen Sammlungsergebnissen zusammen

Wie auch immer Sie dies betrachten, solange Sie eine normalisierte Beziehung wie diese haben, benötigen Sie zwei Abfragen, um ein Ergebnis zu erhalten, das Details aus der Sammlung "Aufgaben" enthält und mit Details aus der Sammlung "Projekte" ausgefüllt wird. MongoDB verwendet in keiner Weise Joins, und Mongoose ist nicht anders. Mongoose bietet .populate() an , aber das ist nur Bequemlichkeitsmagie für das, was im Wesentlichen eine andere Abfrage ausführt und die Ergebnisse für den referenzierten Feldwert zusammenführt.

Dies ist also ein Fall, in dem Sie vielleicht letztendlich erwägen, die Projektinformationen in die Aufgabe einzubetten. Natürlich wird es Duplikate geben, aber es macht die Abfragemuster mit einer einzelnen Sammlung viel einfacher.

Wenn Sie die Sammlungen mit einem referenzierten Modell getrennt halten, haben Sie grundsätzlich zwei Ansätze. Aber zuerst können Sie aggregate verwenden um Ergebnisse zu erhalten, die Ihren tatsächlichen Anforderungen entsprechen:

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

        }
    );

Dabei wird lediglich ein $group verwendet Pipeline, um die Werte von „projectid“ innerhalb der „tasks“-Sammlung zu akkumulieren. Um die Werte für „completed“ und „incomplete“ zu zählen, verwenden wir den $cond Operator, der ein ternärer Operator ist, um zu entscheiden, welcher Wert an <übergeben werden soll code>$summe . Da die erste oder "wenn"-Bedingung hier eine boolesche Auswertung ist, reicht das vorhandene boolesche "complete"-Feld aus, wobei true weitergegeben wird zu "then" oder "else", indem das dritte Argument übergeben wird.

Diese Ergebnisse sind in Ordnung, aber sie enthalten keine Informationen aus der "Projekt"-Sammlung für die gesammelten "_id"-Werte. Ein Ansatz, um die Ausgabe so aussehen zu lassen, besteht darin, die Modellform von .populate() aufzurufen aus dem Aggregationsergebnis-Callback für das zurückgegebene „Ergebnis“-Objekt:

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

In dieser Form die .populate() call nimmt ein Objekt oder Array von Daten als erstes Argument, wobei das zweite ein Optionsdokument für die Population ist, wobei das Pflichtfeld hier für "Pfad" steht. Dadurch werden alle Elemente verarbeitet und aus dem aufgerufenen Modell „aufgefüllt“, indem diese Objekte in die Ergebnisdaten im Callback eingefügt wurden.

Als vollständige Beispielliste:

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

Und das ergibt Ergebnisse wie diese:

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

Also .populate() funktioniert gut für diese Art von Aggregationsergebnis, sogar so gut wie eine andere Abfrage, und sollte im Allgemeinen für die meisten Zwecke geeignet sein. Es gab jedoch ein spezifisches Beispiel in der Auflistung, wo "zwei" Projekte erstellt wurden, aber natürlich nur "eine" Aufgabe, die nur auf eines der Projekte verweist.

Da die Aggregation an der Sammlung „Tasks“ arbeitet, hat sie keinerlei Kenntnis von einem „Projekt“, auf das dort nicht verwiesen wird. Um eine vollständige Liste der "Projekte" mit den berechneten Gesamtsummen zu erhalten, müssen Sie genauer vorgehen, indem Sie zwei Abfragen ausführen und die Ergebnisse "zusammenführen".

Dies ist im Grunde ein "Hash-Merge" für verschiedene Schlüssel und Daten, aber ein netter Helfer dafür ist ein Modul namens nedb , wodurch Sie die Logik konsistenter mit MongoDB-Abfragen und -Operationen anwenden können.

Grundsätzlich möchten Sie eine Kopie der Daten aus der Sammlung "Projekte" mit erweiterten Feldern, dann möchten Sie "zusammenführen" oder .update() diese Informationen mit den Aggregationsergebnissen. Nochmals als vollständige Auflistung zur Demonstration:

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

Und die Ergebnisse hier:

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

Das listet Daten aus jedem "Projekt" auf und enthält die berechneten Werte aus der damit verbundenen "Tasks"-Sammlung.

Es gibt also ein paar Ansätze, die Sie tun können. Auch hier ist es vielleicht am besten, wenn Sie stattdessen einfach "Aufgaben" in die "Projekt"-Elemente einbetten, was wiederum ein einfacher Aggregationsansatz wäre. Und wenn Sie die Aufgabeninformationen einbetten, dann können Sie auch Zähler für „abgeschlossen“ und „unvollständig“ für das „Projekt“-Objekt verwalten und diese einfach aktualisieren, wenn Elemente im Aufgaben-Array mit dem $inc Betreiber.

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

Sie müssen jedoch den aktuellen Aufgabenstatus kennen, damit die Zähleraktualisierungen korrekt funktionieren, aber dies ist einfach zu programmieren, und Sie sollten wahrscheinlich mindestens diese Details in einem Objekt haben, das an Ihre Methoden übergeben wird.

Persönlich würde ich auf die letztere Form ummodellieren und das tun. Sie können Abfragen "zusammenführen", wie hier in zwei Beispielen gezeigt wurde, aber es ist natürlich mit Kosten verbunden.