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

MongoDB fusionerer relaterede samlingselementer med andre samlingsresultater

Uanset hvordan du ser på dette, så længe du har et normaliseret forhold som dette, vil du have brug for to forespørgsler for at få et resultat, der indeholder detaljer fra "opgaver"-samlingen og udfylder med detaljer fra "projekter"-samlingen. MongoDB bruger ikke joins på nogen måde, og mongoose er ikke anderledes. Mongoose tilbyder .populate() , men det er kun bekvemmelighedsmagi for det, der i det væsentlige kører en anden forespørgsel og flette resultaterne på den refererede feltværdi.

Så dette er et tilfælde, hvor du måske i sidste ende kan overveje at integrere projektinformationen i opgaven. Selvfølgelig vil der være duplikering, men det gør forespørgselsmønstrene meget mere enkle med en enkelt samling.

Ved at holde samlingerne adskilt med en refereret model har du grundlæggende to tilgange. Men for det første kan du bruge aggregat for at få resultater mere i forhold til dine faktiske krav:

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

        }
    );

Dette bruger kun en $group pipeline for at akkumulere på værdierne af "projectid" inden for "tasks"-samlingen. For at tælle værdierne for "fuldført" og "ufuldstændig" bruger vi $cond operator, som er en ternær for at bestemme, hvilken værdi der skal overføres til $sum . Da den første eller "hvis"-betingelse her er en boolesk evaluering, vil det eksisterende booleske "komplet"-felt fungere, og videregive hvor true at "derefter" eller "andet" sende det tredje argument.

Disse resultater er okay, men de indeholder ingen information fra "project"-samlingen for de indsamlede "_id"-værdier. En tilgang til at få output til at se sådan ud er at kalde modelformen .populate() fra aggregeringsresultaterne tilbagekald på det returnerede "resultater"-objekt:

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

I denne form er .populate() call tager et objekt eller en række af data som det første argument, hvor det andet er et option-dokument for populationen, hvor det obligatoriske felt her er for "sti". Dette vil behandle alle elementer og "udfylde" fra den model, der blev kaldt indsættelse af disse objekter i resultatdataene i tilbagekaldet.

Som et komplet eksempel på listen:

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

Og dette vil give resultater som sådan:

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

.populate() fungerer godt til denne form for aggregeringsresultat, selv lige så effektivt som en anden forespørgsel, og burde generelt være egnet til de fleste formål. Der var dog et specifikt eksempel inkluderet i listen, hvor der er oprettet "to" projekter, men selvfølgelig kun "én" opgave, der refererer til kun ét af projekterne.

Da aggregering arbejder på "opgaver"-samlingen, har den intet kendskab til noget "projekt", der ikke er refereret der. For at få en komplet liste over "projekter" med de beregnede totaler skal du være mere specifik med at køre to forespørgsler og "sammenslå" resultaterne.

Dette er dybest set en "hash-fusion" på forskellige nøgler og data, men en god hjælper til dette er et modul kaldet nedb , som giver dig mulighed for at anvende logikken på en måde, der er mere konsistent med MongoDB-forespørgsler og operationer.

Grundlæggende vil du have en kopi af data fra "projekter"-samlingen med udvidede felter, så vil du "flette" eller .update() denne information med aggregeringsresultaterne. Igen som en komplet liste for at demonstrere:

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

Og resultaterne her:

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

Det viser data fra hvert "projekt" og inkluderer de beregnede værdier fra samlingen "opgaver", der er relateret til det.

Så der er et par fremgangsmåder, du kan gøre. Igen kan det i sidste ende være bedst for dig blot at indlejre "opgaver" i "projekt"-elementerne i stedet for, hvilket igen ville være en simpel aggregeringstilgang. Og hvis du skal indlejre opgaveoplysningerne, så kan du lige så godt vedligeholde tællere for "fuldført" og "ufuldstændig" på "projekt"-objektet og blot opdatere disse, efterhånden som elementer er markeret som afsluttet i opgave-arrayet med $inc operatør.

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

Du skal dog kende den aktuelle opgavestatus for at tælleropdateringerne fungerer korrekt, men det er nemt at kode for, og du burde sandsynligvis have mindst disse detaljer i et objekt, der passerer ind i dine metoder.

Personligt ville jeg ommodellere til sidstnævnte form og gøre det. Du kan lave forespørgslen "fletning", som det er vist i to eksempler her, men det koster selvfølgelig noget.



  1. Sådan projiceres/filtreres indlejrede nøgler ved at anvende regex på nøgle

  2. Mongoose:Samling udfyldes ikke, når den bruges som ref i et andet dokument

  3. Sådan forbinder du lokal Mongo-database til docker

  4. Mongodb $graphLookup opbygningshierarki