Ramon
Ramon

Reputation: 73

Mongoose virtual populate and aggregates

I'm trying to do aggregations on Mongoose schemas that use the newer virtual populate functionality (using Mongoose 4.13, Mongo 3.6).

Lets say I have the following (simplified for illustration purposes) schemas:

const ProjectSchema = new mongoose.Schema({
  projectId: Number,
  description: String
});

ProjectSchema.virtual('tasks', {
  ref: 'Task',
  localField: 'projectId',
  foreignField: 'projectId' 
  justOne: false
]);

const TaskSchema = new mongoose.Schema({
  taskId: Number,
  projectId: Number
  hours: Number
});

const Project = mongoose.model('Project', ProjectSchema);
const Task = mongoose.model('Task', TaskSchema);

Querying on Project and populating relating tasks is working fine using .populate() like:

Project.find({projectId: <id>}).populate('tasks');

But now I would like to sum the hours on tasks by project (left the $sum part out below btw..). I'm only getting empty arrays back no matter what. Isn't it possible to aggregate with virtual populate or?

const result = await Project.aggregate([
   { $match : { projectId: <id> } },
   { $lookup: { 
       from: 'tasks',
       localField: 'projectId',
       foreignField: 'projectId',
       as: 'tasks'
     },
     {
        $unwind: '$tasks' 
     }
   }
 ]);

Upvotes: 4

Views: 6046

Answers (3)

ruwan800
ruwan800

Reputation: 1887

mongoose >= 3.6 supports the Model.populate() method.

So, You can pass the result of the aggregate method to the populate method like this.

var opts = [{ path: 'tasks'}];
const populatedResult = await Project.populate(result, opts);

Upvotes: 1

Jason Cust
Jason Cust

Reputation: 10899

Referencing mongoose's documentation for Model.aggregate:

Arguments are not cast to the model's schema because $project operators allow redefining the "shape" of the documents at any stage of the pipeline, which may leave documents in an incompatible format.

Essentially this means the magic of anything mongoose allows via using a schema is not applied when using aggregates. In fact, you would need to refer directly to MongoDB's documentation for aggregation (specifically $lookup).

const ProjectSchema = new mongoose.Schema({
  projectId: Number,
  description: String
});

const TaskSchema = new mongoose.Schema({
  taskId: Number,
  projectId: Number,
  hours: Number
});

const Project = connection.model('Project', ProjectSchema);
const Task = connection.model('Task', TaskSchema);

const project1 = new Project({ projectId: 1, description: 'Foo'});
const project2 = new Project({ projectId: 2, description: 'Foo'});
const task1 = new Task({ task: 1, projectId: 1, hours: 1});
const task2 = new Task({ task: 2, projectId: 1, hours: 2});
const task3 = new Task({ task: 3, projectId: 2, hours: 1});

Promise.all([
  project1.save(),
  project2.save(),
  task1.save(),
  task2.save(),
  task3.save()
]).then(() => {
  return Project.aggregate([
    { $match: { projectId: 1 }},
    {
      $lookup: {
        from: 'tasks',
        localField: 'projectId',
        foreignField: 'projectId',
        as: 'tasks'
      }
    }
  ]).exec();
}).then((result) => {
  console.dir(result, { depth: null, color: true });
});

Outputs the following:

[{
  _id: ObjectID {/*ID*/},
  projectId: 1,
  description: 'Foo',
  __v: 0,
  tasks: [{
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 2,
      __v: 0
    },
    {
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 1,
      __v: 0
    }
  ]
}]

But you might ask: "That's basically what I have so why does that work?!"

I am not sure if your example code was copy/pasted but in the example provided the $unwind stage appears to have been accidentally included in the 2nd stage ($lookup) of your pipeline. If you were to add it as a 3rd stage as the following code shows then the output would be altered as shown.

return Project.aggregate([
  { $match: { projectId: 1 }},
  {
    $lookup: {
      from: 'tasks',
      localField: 'projectId',
      foreignField: 'projectId',
      as: 'tasks'
    }
  },
  { $unwind: '$tasks' }
]).exec();

Outputs:

[{
    _id: ObjectID {/*ID*/},
    projectId: 1,
    description: 'Foo',
    __v: 0,
    tasks: {
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 1,
      __v: 0
    }
  },
  {

    _id: ObjectID {/*ID*/},
    projectId: 1,
    description: 'Foo',
    __v: 0,
    tasks: {
      _id: ObjectID {/*ID*/},
      projectId: 1,
      hours: 2,
      __v: 0
    }
  }
]

Upvotes: 2

vasylOk
vasylOk

Reputation: 566

Virtual populate won't work with aggregate, because "The documents returned are plain javascript objects, not mongoose documents (since any shape of document can be returned)."(c), more information in the docs - Mongoose aggregate

Upvotes: 1

Related Questions