Aggregate and lookup - node.js

I am trying to group a mongoose query and populate a few sub-documents in mongoose using $lookup but my result document arrays are returning empty.
Can anyone tell me why?
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
var answerSchema = new Schema({
_user : { type: Schema.Types.ObjectId, ref: 'User', required: true },
_poll : { type: Schema.Types.ObjectId, ref: 'Poll', required: true },
_question : [{ type: Schema.Types.ObjectId, ref: 'Question' }],
answer : { type : String },
},
{ timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
});
mongoose.model('Answer', answerSchema);
Here's my code:
module.exports = {
index: function(req, res, next){
Poll.findOne({_id: req.params.poll_id}, function(err, poll){
if(err) return next(err);
console.log(poll);
Answer.aggregate([
{$unwind: "$_question"},
{$match: {_poll: poll._id}},
{$group: {
_id: '$_question',
}},
{
$lookup : {
from : 'polls',
localField : '_poll',
foreignField: '_id',
as: 'poll'
},
},
{
$lookup : {
from : 'questions',
localField : '_question',
foreignField: '_id',
as: 'questions'
},
},
{
$lookup : {
from : 'users',
localField : '_user',
foreignField: '_id',
as: 'user'
},
},
], function (err, result) {
res.status(200).json(result);
});
});
},
The new subdocuments are returned empty for some reason.
Please note that each answer contains the reference to ONE poll, ONE user and MANY questions.
[
{
_id: "57ce8927ea5a4f090774215d",
poll: [ ],
questions: [ ],
user: [ ]
}
]
Can anyone spot my mistake?
Should I be using $project instead? I heard $lookup is the new and better way. I am using mongo 3.2.0 and mongoose 4.5.8.

Mongdb aggregate query is a pipeline operation. So the result of subsequent queries is passed on to the next query. For more info on Mongodb aggregate refer this. The mistake you have done is that when you used the $group query only _id is being passed on to the next $lookup query. You can fix this by using the following query.
Answer.aggregate([
{$unwind: "$_question"},
{$match: {_poll: poll._id}},
{$group: {
_id: '$_question',
_poll: { $first: '$_poll'},
_user: { $first: '$_user'},
_question : { $first: "$_question "}
}},
{
$lookup : {
from : 'polls',
localField : '_poll',
foreignField: '_id',
as: 'poll'
},
},
{
$lookup : {
from : 'questions',
localField : '_question',
foreignField: '_id',
as: 'questions'
},
},
{
$lookup : {
from : 'users',
localField : '_user',
foreignField: '_id',
as: 'user'
},
},
], function (err, result) {
res.status(200).json(result);
});

Related

Aggregate Function Mongoose - Node

I have a schema
const membershipsSchema = new Schema({
spaceId: {
type: Schema.Types.ObjectId,
ref: 'Space',
},
member: {
type: Schema.Types.ObjectId,
ref: 'User',
},
....
);
mongoose.model('Membership', membershipsSchema);
I want to use join statement like
Select * from membershipPlans as plans join User as users on plans.member=users._id
where plans.spaceId=id and users.status <> 'archived'; // id is coming from function arguments
I tried the aggregate pipeline like
const memberships = await Memberships.aggregate([
{
$match: {
spaceId: id
}
},
{
$lookup: {
from: 'User',
localField: 'member',
foreignField: '_id',
as: 'users',
},
},
{
$match: {
'users.status': {$ne: 'archived'}
}
},
]);
But on console.log(memberships); I am getting an empty array. If I try return Memberships.find({ spaceId: id }) it returns populated memberships of that space. But when I try
const memberships = await Memberships.aggregate([
{
$match: {
spaceId: id
}
},
]}
It still returns an empty array. Not sure if I know how to use an aggregate pipeline.
There are two things that you need to do:
Cast id to ObjectId.
Instead of using $match, just filter the contents of the users array using $filter.
Try this:
const memberships = await Memberships.aggregate([
{
$match: {
spaceId: new mongoose.Types.ObjectId(id)
}
},
{
$lookup: {
from: 'User',
localField: 'member',
foreignField: '_id',
as: 'users',
},
},
{
$project: {
users: {$filter: {
input: "$users",
as: "user",
cond: {
$ne: ["$$user.status", "archived"]
}
}}
}
},
]);

How to get the first element from a child lookup in aggregation - Mongoose

I'm trying to find all the docs from groupUserRoleSchema with a specific $match condition in the child. I'm getting the expected result, but the child application inside groupSchema is coming as an array.
I just need the first element from the application array as an object. How to convert this application into a single object.
These are my models
const groupUserRoleSchema = new mongoose.Schema({
group: {
type: mongoose.Schema.Types.ObjectId,
ref: 'group'
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'user'
}
});
const groupSchema = new mongoose.Schema({
application: {
type: mongoose.Schema.Types.ObjectId,
ref: 'application'
}
});
Here is my aggregate condition.
groupUserRoleModel.aggregate([
{
$lookup: {
from: "groups", //must be PHYSICAL collection name
localField: "group",
foreignField: "_id",
as: "group",
}
},
{
$lookup: {
from: "users",
localField: "user",
foreignField: "_id",
as: "user",
}
},
{
$lookup: {
from: "applications",
localField: "group.application",
foreignField: "_id",
as: "group.application",
}
},
{
$addFields: {
group: {
$arrayElemAt: ["$group", 0],
},
user: {
$arrayElemAt: ["$user", 0],
}
},
},
{
$match: {
"user.email_id": requestHeaders.user_email
},
}
]);
Here the $lookup group.application is coming as an array. Instead i need it as an object.
Below added is the current output screen-shot
Here is the expected output screen-shot
Any suggestions ?
A alternative is re-writing the main object return, and merge the fields.
The lookup field, gonna use the $arrayElemAt attribute at position 0, with the combination of the root element '$$ROOT'.
const result = await groupUserRoleModel.aggregate([
{
$match: {
"user.email_id": requestHeaders.user_email //The Match Query
},
},
{
$lookup: {
from: 'groups', // The collection name
localField: 'group',
foreignField: '_id',
as: 'group', // Gonna be a group
},
},
{
// Lookup returns a array, so get the first once it is a _id search
$replaceRoot: {
newRoot: {
$mergeObjects: [ // merge the object
'$$ROOT',// Get base object
{
store: {
$arrayElemAt: ['$group', 0], //Get first elemnt
},
},
],
},
},
},
]);

Filter results using $match in MongoDB aggregate returning blank array

I have the following schema:
const UserQualificationSchema = new Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
qualification: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Qualification',
},
expiry_date: {
type: Date
}
}
const QualificationSchema = new Schema(
{
fleet: {
type: [String], // Eg ["FleetA", "FleetB", "FleetC"]
required: true,
}
}
I am searching the UserQualifications with filters in a table, to search them by fleet, qualification or expiry date. I so far have the following aggregate:
db.UserQualifications.aggregate([{
{
$lookup: {
from: 'qualifications',
localField: 'qualification',
foreignField: '_id',
as: 'qualification',
},
},
{
$unwind: '$qualification',
},
{
$match: {
$and: [
'qualification.fleet': {
$in: ["Fleet A", "Fleet C"], // This works
},
expiry_date: {
$lt: req.body.expiry_date, // This works
},
qualification: { // Also tried 'qualification._id'
$in: ["6033e4129070031c07fbbf29"] // Adding this returns blank array
}
]
},
}
}])
Filtering by fleet, and expiry date both work, independently and in combination, however when adding by the qualification ID, it returns blank despite the ID's being sent in being valid.
Am i missing something here?
Looking at your schema I can infer that qualification in ObjectId and in the query you are passing only the string value of ObjectId. You can pass the ObjectId to get your expected output
db.UserQualifications.aggregate([
{
$lookup: {
from: "Qualifications",
localField: "qualification",
foreignField: "_id",
as: "qualification",
},
},
{
$unwind: "$qualification",
},
{
$match: {
"qualification.fleet": {
$in: [
"FleetA",
"FleetC"
],
},
expiry_date: {
$lt: 30 // some dummy value to make it work
},
"qualification._id": {
$in: [
// some dummy value to make it work
ObjectId("5a934e000102030405000000")
]
}
},
}
])
I have created a playground with some dummy data to test the query: Mongo Playground
Also, In $match stage there is no need to combine query explicitly in $and as by default behaviour will be same as $and only so I have remove that part in my query

mongodb lookup (aggregation)

I have this function and I can't get it to work.
function GetDataTeacher(err){
Data_teacher.aggregate([
{
"$project": {
"user": {
"$toString": "$user"
}
}
},
{
$lookup:
{
from: "Teacher",
localField: "user",
foreignField: "user",
as: "info"
}
}
]).exec(function(err, results){
console.log(results);
});
}
my models have in common
user: { type: Schema.ObjectId, ref: 'User' },
I don't understand what I can be doing wrong
Possible issues here are:
1, You are converting the user id to string(which was already of type id)
user: { type: Schema.ObjectId, ref: 'User' },
so no need of converting to string and you can do this directly.
Data_teacher.aggregate([
{
$lookup:
{
from: "users",
localField: "user",
foreignField: "user",
as: "info"
}
}
])
2, Please check if you have the user field in both the collections (Data_teacher and Teacher).
3, check if localfield and foreignfield are correct

Mongoose $group & $lookup aggregation returns empty array with $unwind

I have 2 schemas TravelRoute & Flights. I am trying to find the $min of Flight.fare.total_price and return that result with some details from TravelRoute which is ref-ed in the Flights schema. I am using Mongo 3.4.1 and Mongoose 4.8.5
const TravelRoute = new mongoose.Schema({
departureAirport: {
type: String,
required: true,
trim: true,
},
arrivalAirport: {
type: String,
required: true,
trim: true,
},
durationDays: {
type: Number
},
fromNow: {
type: Number
},
isActive: {
type: Boolean
},
class: {
type: String,
enum: ['economy', 'business', 'first', 'any']
}
}
const Flights = new mongoose.Schema({
route: {
type: mongoose.Schema.Types.ObjectId, ref: 'TravelRoute'
},
departure_date: {
type: Date
},
return_date: {
type: Date
},
fare: {
total_price: {
type: Number
}
}
}
I have the following code:
Flights.aggregate(
{$group: {
_id: {
route: '$route',
departure_date: '$departure_date',
return_date: '$return_date'
},
total_price: {$min: '$fare.total_price'}
}
},
{$lookup: {
from: 'TravelRoute',
localField: '_id.route',
foreignField: '_id',
as: 'routes'
}},
{$unwind: '$routes'},
{$project: {
'routes.departureAirport': 1,
'routes.destinationAirport': 1,
'departure_date': 1,
'return_date': 1,
'total_price': 1,
'_id': 1
}},
{$sort: {'total_price': 1}},
function(err, cheapFlights){
if (err) {
log.error(err)
return next(new errors.InvalidContentError(err.errors.name.message))
}
res.send(cheapFlights)
next()
})
The above returns an empty array [] when I use $unwind. When I comment out the $unwind stage, it returns the following:
[
{
"_id": {
"route": "58b30cac0efb1c7ebcd14e0a",
"departure_date": "2017-04-03T00:00:00.000Z",
"return_date": "2017-04-08T00:00:00.000Z"
},
"total_price": 385.6,
"routes": []
},
{
"_id": {
"route": "58ae47ddc30b175150d94eef",
"departure_date": "2017-04-03T00:00:00.000Z",
"return_date": "2017-04-10T00:00:00.000Z"
},
"total_price": 823.68,
"routes": []
}
...
I'm reasonably new to Mongo and Mongoose. I am confounded by the the fact that it returns "routes" even though I don't project that. And it doesn't return departure_date (etc) even though I ask for it? I am not sure I need $unwind as there will only be one TravelRoute per Flight.
Thanks...
Edit
Here was the final solution:
{$lookup: {
from: 'travelroutes', //<-- this is the collection name in the DB
localField: '_id.route',
foreignField: '_id',
as: 'routes'
}},
{$unwind: '$routes'},
{$project: {
departureAirport: '$routes.departureAirport',
arrivalAirport: '$routes.arrivalAirport',
departureDate: '$_id.departure_date',
returnDate: '$_id.return_date',
'total_price': 1,
'routeID': '$_id.route',
'_id': 0
}},
{$sort: {'total_price': 1}},

Resources