MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

MongoDB mesclar a contagem de itens de coleção relacionados com outros resultados de coleção


Seja qual for a maneira de olhar para isso, contanto que você tenha um relacionamento normalizado como esse, você precisaria de duas consultas para obter um resultado contendo detalhes da coleção "tarefas" e preenchendo com detalhes da coleção "projetos". O MongoDB não usa junções de forma alguma, e o mongoose não é diferente. O Mongoose oferece .populate() , mas isso é apenas uma mágica de conveniência para o que está essencialmente executando outra consulta e mesclando os resultados no valor do campo referenciado.

Portanto, este é um caso em que talvez você possa considerar incorporar as informações do projeto na tarefa. É claro que haverá duplicação, mas torna os padrões de consulta muito mais simples com uma coleção singular.

Mantendo as coleções separadas com um modelo referenciado, você basicamente tem duas abordagens. Mas primeiro você pode usar aggregate para obter resultados mais de acordo com suas necessidades reais:
      Task.aggregate(
        [
          { "$group": {
            "_id": "$projectId",
            "completed": {
              "$sum": {
                "$cond": [ "$completed", 1, 0 ]
              }
            },
            "incomplete": {
              "$sum": {
                "$cond": [ "$completed", 0, 1 ]
              }
            }
          }}
        ],
        function(err,results) {

        }
    );

Isso simplesmente usa um $group pipeline para acumular os valores de "projectid" na coleção "tasks". Para contar os valores de "concluído" e "incompleto", usamos o $cond operador que é um ternário para decidir qual valor passar para $sum . Como a primeira condição ou "se" aqui é uma avaliação booleana, o campo booleano "completo" existente fará, passando onde true para "then" ou "else" passando o terceiro argumento.

Esses resultados estão corretos, mas não contêm nenhuma informação da coleção "project" para os valores "_id" coletados. Uma abordagem para fazer a saída parecer dessa forma é chamar o formulário de modelo de .populate() de dentro do retorno de chamada dos resultados da agregação no objeto "results" retornado:
    Project.populate(results,{ "path": "_id" },callback);

Neste formulário o .populate() call recebe um objeto ou array de dados como seu primeiro argumento, sendo o segundo um documento de opções para a população, onde o campo obrigatório aqui é para "path". Isso processará todos os itens e "preencherá" o modelo que foi chamado inserindo esses objetos nos dados de resultados no retorno de chamada.

Como uma lista de exemplo completa:
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();
  }
);

E isso dará resultados assim:
[
    {
        "_id": {
            "_id": "54beef3178ef08ca249b98ef",
            "name": "Project2",
            "__v": 0
        },
        "completed": 0,
        "incomplete": 1
    }
]

Então .populate() funciona bem para esse tipo de resultado de agregação, mesmo com a mesma eficácia de outra consulta, e geralmente deve ser adequado para a maioria dos propósitos. No entanto, houve um exemplo específico incluído na listagem onde existem "dois" projetos criados, mas é claro que apenas "uma" tarefa referenciando apenas um dos projetos.

Como a agregação está trabalhando na coleção de "tarefas", ela não tem conhecimento de nenhum "projeto" que não esteja referenciado lá. Para obter uma lista completa de "projetos" com os totais calculados, você precisa ser mais específico ao executar duas consultas e "mesclar" os resultados.

Este é basicamente um "hash merge" em chaves e dados distintos, porém um bom auxiliar para isso é um módulo chamado nedb , que permite aplicar a lógica de maneira mais consistente com as consultas e operações do MongoDB.

Basicamente você quer uma cópia dos dados da coleção "projects" com campos aumentados, então você quer "merge" ou .update() essa informação com os resultados da agregação. Novamente como uma lista completa para demonstrar:
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();
  }

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

Isso lista os dados de cada "projeto" e inclui os valores calculados da coleção de "tarefas" relacionada a ele.

Portanto, existem algumas abordagens que você pode fazer. Novamente, você pode ser melhor apenas incorporar "tarefas" nos itens do "projeto", o que novamente seria uma abordagem de agregação simples. E se você for incorporar as informações da tarefa, também poderá manter contadores para "completo" e "incompleto" no objeto "projeto" e simplesmente atualizá-los à medida que os itens são marcados como concluídos na matriz de tarefas com o $inc operador.
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
);

Você precisa saber o status atual da tarefa para que as atualizações do contador funcionem corretamente, mas isso é fácil de codificar e você provavelmente deve ter pelo menos esses detalhes em um objeto passando para seus métodos.

Pessoalmente, eu remodelaria para a última forma e faria isso. Você pode fazer a "fusão" de consultas, como foi mostrado em dois exemplos aqui, mas é claro que isso tem um custo.