sql >> Databáze >  >> NoSQL >> MongoDB

MongoDB sloučit počet položek sbírky související s ostatními výsledky sbírky

Ať už se na to díváte z jakéhokoli úhlu, pokud máte takto normalizovaný vztah, budete potřebovat dva dotazy, abyste získali výsledek obsahující podrobnosti z kolekce „úkoly“ a vyplnění podrobnostmi z kolekce „projekty“. MongoDB spojení v žádném případě nepoužívá a u mongoose tomu není jinak. Mongoose nabízí .populate() , ale to je pouze kouzlo pohodlí pro to, co v podstatě spouští jiný dotaz a slučuje výsledky s hodnotou odkazovaného pole.

Toto je jeden případ, kdy možná nakonec můžete zvážit vložení informací o projektu do úkolu. Samozřejmě dojde k duplikaci, ale díky jednotné kolekci jsou vzory dotazů mnohem jednodušší.

Udržování sbírek oddělených pomocí referenčního modelu pak máte v zásadě dva přístupy. Nejprve však můžete použít agregát abyste dosáhli výsledků více podle vašich skutečných požadavků:

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

        }
    );

Toto pouze používá $group potrubí za účelem akumulace na hodnotách "projectid" v kolekci "úkoly". Abychom spočítali hodnoty pro "completed" a "incomplete" používáme $cond operátor, který je ternární k rozhodnutí, kterou hodnotu předat $sum . Vzhledem k tomu, že první nebo „if“ podmínka je zde booleovské vyhodnocení, bude fungovat existující booleovské pole „complete“ a předá tam, kde je true k "pak" nebo "jinak" předání třetího argumentu.

Tyto výsledky jsou v pořádku, ale neobsahují žádné informace z kolekce "project" pro shromážděné hodnoty "_id". Jedním z přístupů k tomu, aby výstup vypadal tímto způsobem, je zavolat modelovou formu .populate() ze zpětného volání výsledků agregace na vrácený objekt "výsledky":

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

V tomto tvaru .populate() call bere objekt nebo pole dat jako svůj první argument, přičemž druhý je dokument možností pro populaci, kde povinné pole je zde pro "cestu". To zpracuje všechny položky a "vyplní" z modelu, který byl nazván vložením těchto objektů do dat výsledků ve zpětném volání.

Jako úplný příklad výpisu:

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

A to dá výsledky jako:

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

Takže .populate() funguje dobře pro tento druh výsledku agregace, dokonce stejně efektivně jako jiný dotaz, a měl by být obecně vhodný pro většinu účelů. Ve výpisu však byl uveden konkrétní příklad, kde jsou vytvořeny „dva“ projekty, ale samozřejmě pouze „jeden“ úkol odkazující pouze na jeden z projektů.

Vzhledem k tomu, že agregace pracuje na kolekci „úkolů“, nemá vůbec žádné znalosti o žádném „projektu“, který tam není uveden. Chcete-li získat úplný seznam „projektů“ s vypočtenými součty, musíte být konkrétnější při spuštění dvou dotazů a „sloučení“ výsledků.

Toto je v podstatě "hash merge" na odlišných klíčích a datech, nicméně příjemným pomocníkem je modul s názvem nedb , což vám umožňuje aplikovat logiku způsobem, který je konzistentnější s dotazy a operacemi MongoDB.

V podstatě chcete kopii dat z kolekce "projekty" s rozšířenými poli, pak chcete "sloučit" nebo .update() tyto informace s výsledky agregace. Opět jako úplný seznam pro demonstraci:

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

A výsledky zde:

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

To uvádí data z každého „projektu“ a zahrnuje vypočítané hodnoty z kolekce „úkolů“, která s ním souvisí.

Existuje tedy několik přístupů, které můžete udělat. Opět můžete být nakonec nejlepší, když místo toho vložíte „úkoly“ do položek „projekt“, což by byl opět jednoduchý agregační přístup. A pokud se chystáte vložit informace o úkolu, můžete také udržovat počítadla pro "dokončeno" a "nedokončeno" na objektu "projekt" a jednoduše je aktualizovat, protože položky jsou označeny jako dokončené v poli úkolů pomocí $inc operátor.

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

Aby aktualizace počítadla fungovaly správně, musíte znát aktuální stav úlohy, ale kódování je snadné a pravděpodobně byste měli mít alespoň tyto podrobnosti v objektu, který přechází do vašich metod.

Osobně bych přemodeloval na druhou formu a udělal to. Můžete provést dotaz "sloučení", jak bylo ukázáno ve dvou příkladech zde, ale samozřejmě to něco stojí.



  1. MongoDB aktualizuje pole ve vnořeném poli

  2. Jarní mongo šablona. Řazení nefunguje v geografickém dotazu (NearQuery)

  3. Přeměna scénáře setí Mongoose na příslib

  4. Mongoose - najít poslední zprávu od každého uživatele