MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

MongoDB fusionne le nombre d'éléments de collection liés avec d'autres résultats de collection

Quelle que soit la façon dont vous voyez cela, tant que vous avez une relation normalisée comme celle-ci, vous aurez besoin de deux requêtes pour obtenir un résultat contenant les détails de la collection "tâches" et remplissant les détails de la collection "projets". MongoDB n'utilise aucunement les jointures, et la mangouste n'est pas différente. Mongoose propose .populate() , mais ce n'est que de la magie pratique pour ce qui exécute essentiellement une autre requête et fusionne les résultats sur la valeur de champ référencée.

Il s'agit donc d'un cas où vous pourriez éventuellement envisager d'intégrer les informations du projet dans la tâche. Bien sûr, il y aura des doublons, mais cela rend les modèles de requête beaucoup plus simples avec une collection unique.

En gardant les collections séparées avec un modèle référencé, vous avez essentiellement deux approches. Mais vous pouvez d'abord utiliser aggregate afin d'obtenir des résultats plus conformes à vos besoins réels :

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

        }
    );

Cela utilise simplement un $group pipeline afin d'accumuler sur les valeurs de "projectid" dans la collection "tasks". Afin de compter les valeurs pour "completed" et "incomplete" nous utilisons le $cond opérateur qui est un ternaire pour décider quelle valeur passer à $sum . Étant donné que la première condition ou "si" ici est une évaluation booléenne, alors le champ booléen "complet" existant fera l'affaire, en passant où true à "then" ou "else" en passant le troisième argument.

Ces résultats sont corrects mais ils ne contiennent aucune information de la collection "project" pour les valeurs "_id" collectées. Une approche pour donner à la sortie cet aspect consiste à appeler le formulaire de modèle de .populate() à partir du rappel des résultats d'agrégation sur l'objet "résultats" renvoyé :

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

Sous cette forme le .populate() call prend un objet ou un tableau de données comme premier argument, le second étant un document d'options pour la population, où le champ obligatoire ici est pour "chemin". Cela traitera tous les éléments et les "remplira" à partir du modèle qui a été appelé en insérant ces objets dans les données de résultats dans le rappel.

En guise d'exemple complet :

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

Et cela donnera des résultats comme ceci :

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

Donc .populate() fonctionne bien pour ce type de résultat d'agrégation, même aussi efficacement qu'une autre requête, et devrait généralement convenir à la plupart des objectifs. Il y avait cependant un exemple spécifique inclus dans la liste où il y a "deux" projets créés mais bien sûr seulement "une" tâche faisant référence à un seul des projets.

Étant donné que l'agrégation travaille sur la collection "tâches", elle n'a aucune connaissance d'aucun "projet" qui n'y est pas référencé. Afin d'obtenir une liste complète de "projets" avec les totaux calculés, vous devez être plus précis en exécutant deux requêtes et en "fusionnant" les résultats.

Il s'agit essentiellement d'une "fusion de hachage" sur des clés et des données distinctes, mais une bonne aide pour cela est un module appelé nedb , ce qui vous permet d'appliquer la logique de manière plus cohérente avec les requêtes et opérations MongoDB.

En gros, vous voulez une copie des données de la collection "projets" avec des champs augmentés, puis vous voulez "fusionner" ou .update() ces informations avec les résultats de l'agrégation. Encore une fois comme une liste complète pour démontrer :

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

Et les résultats ici :

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

Cela répertorie les données de chaque "projet" et inclut les valeurs calculées de la collection "tâches" qui lui est associée.

Il y a donc quelques approches que vous pouvez faire. Encore une fois, vous feriez peut-être mieux d'intégrer des "tâches" dans les éléments du "projet", ce qui serait à nouveau une approche d'agrégation simple. Et si vous envisagez d'intégrer les informations sur la tâche, vous pouvez également conserver des compteurs pour "complet" et "incomplet" sur l'objet "projet" et simplement les mettre à jour lorsque les éléments sont marqués comme terminés dans le tableau des tâches avec le $inc opérateur.

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

Vous devez cependant connaître l'état actuel de la tâche pour que les mises à jour du compteur fonctionnent correctement, mais c'est facile à coder et vous devriez probablement avoir au moins ces détails dans un objet passant dans vos méthodes.

Personnellement, je remodelerais cette dernière forme et je le ferais. Vous pouvez effectuer une "fusion" de requêtes comme cela a été montré dans deux exemples ici, mais cela a bien sûr un coût.