sql >> Databasteknik >  >> NoSQL >> MongoDB

MongoDB slår samman relaterade samlingsobjekt med andra samlingsresultat

Oavsett hur du ser på det här, så länge du har en normaliserad relation som denna så skulle du behöva två frågor för att få ett resultat som innehåller detaljer från "tasks"-samlingen och fyller i med detaljer från "projects"-samlingen. MongoDB använder inte joins på något sätt, och mongoose är inte annorlunda. Mongoose erbjuder .populate() , men det är bara bekvämlighetsmagi för det som i huvudsak kör en annan fråga och slår samman resultaten på det refererade fältvärdet.

Så det här är ett fall där du kanske i slutändan kan överväga att bädda in projektinformationen i uppgiften. Naturligtvis kommer det att finnas dubblering, men det gör frågemönstren mycket enklare med en singulär samling.

Genom att hålla samlingarna åtskilda med en refererad modell har du i princip två tillvägagångssätt. Men först kan du använda aggregat för att få resultat mer efter dina faktiska krav:

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

        }
    );

Detta använder bara en $group pipeline för att ackumulera på värdena för "projectid" inom samlingen "tasks". För att räkna värdena för "completed" och "incomplete" använder vi $cond operator som är en ternär för att bestämma vilket värde som ska skickas till $sum . Eftersom det första eller "om"-villkoret här är en boolesk utvärdering, kommer det befintliga booleska "fullständiga" fältet att fungera, och skickar vidare där true att "då" eller "annat" skicka det tredje argumentet.

Dessa resultat är okej men de innehåller ingen information från "project"-samlingen för de insamlade "_id"-värdena. Ett sätt att få utdata att se ut så här är att anropa modellformen .populate() inifrån aggregeringsresultatet callback på det returnerade "results"-objektet:

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

I den här formen .populate() call tar ett objekt eller en datamatris som sitt första argument, där det andra är ett alternativdokument för populationen, där det obligatoriska fältet här är för "sökväg". Detta kommer att bearbeta alla objekt och "befolka" från modellen som kallades för att infoga dessa objekt i resultatdata i återuppringningen.

Som ett komplett exempel:

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

Och detta ger resultat som så här:

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

.populate() fungerar bra för den här typen av aggregeringsresultat, till och med lika effektivt för en annan fråga, och bör i allmänhet vara lämplig för de flesta ändamål. Det fanns dock ett specifikt exempel i listan där det skapades "två" projekt men naturligtvis bara "en" uppgift som refererar till bara ett av projekten.

Eftersom aggregering arbetar med "tasks"-samlingen har den ingen som helst kunskap om något "projekt" som inte refereras där. För att få en komplett lista över "projekt" med de beräknade totalsummorna måste du vara mer specifik när du kör två frågor och "slår samman" resultaten.

Detta är i grund och botten en "hash-sammanfogning" på distinkta nycklar och data, men en bra hjälp för detta är en modul som heter nedb , vilket gör att du kan tillämpa logiken på ett sätt som är mer överensstämmande med MongoDB-frågor och operationer.

I grund och botten vill du ha en kopia av data från "projekt"-samlingen med utökade fält, sedan vill du "sammanfoga" eller .update() den informationen med aggregeringsresultaten. Återigen som en komplett lista för att demonstrera:

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

Och resultaten här:

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

Det listar data från varje "projekt" och inkluderar de beräknade värdena från "tasks"-samlingen som är relaterade till det.

Så det finns några metoder du kan göra. Återigen, du kanske i slutändan är bäst av att bara bädda in "uppgifter" i "projekt"-objekten istället, vilket återigen skulle vara en enkel aggregeringsmetod. Och om du ska bädda in uppgiftsinformationen kan du lika gärna behålla räknare för "complete" och "incomplete" på "project"-objektet och helt enkelt uppdatera dessa allt eftersom objekt markeras som slutförda i tasks-arrayen 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 måste dock känna till den aktuella uppgiftsstatusen för att räknaruppdateringarna ska fungera korrekt, men det är lätt att koda för och du bör förmodligen ha åtminstone dessa detaljer i ett objekt som passerar in i dina metoder.

Personligen skulle jag göra om till den senare formen och göra det. Du kan göra frågan "sammanslagning" som har visats i två exempel här, men det kostar naturligtvis.



  1. gruppera efter frågor om meteorsamling

  2. Genomsnittligt ett underdokumentfält över dokument i Mongo

  3. mongodb javascript uppdatering

  4. Extrahera, modellera och ändra datamodell, med mongoid/mongodb