Mongodb join query from nodejs - node.js

I am new to MongoDB and trying to join two query and store result in a single model. I want to fetch client name from another collection while fetching client task.
Model:-
const mongoose = require('mongoose');
var ObjectId = mongoose.Schema.Types.ObjectId;
const ClientTaskSchema = new mongoose.Schema({
clientId: {
type: Number
},
taskId: {
type: Number
},
clientTaskId: {
type: Number
},
active: {
type: Boolean
}
});
module.exports = mongoose.model('ClientTask', ClientTaskSchema);
Controller:-
module.exports.getClientByTask = function(req, res) {
var query = url.parse(req.url,true).query;
ClientTask.find({taskId: query.taskId}, function(err, clientTask) {
if (err) throw err;
if (!clientTask) {
res.status(200).json({ success: false, message: 'Somthing went wrong. Please contact admin.'});
}
else {
res.status(200).json({ success: true, message: 'Successfull', data: clientTask});
}
});
};

One option is to pass clientId as a reference:
clientId: {
type: mongoose.Schema.Types.ObjectId, ref: 'Client / or whatever your model'
}
Then you'll be able to use Mongoose's populate method http://mongoosejs.com/docs/populate.html
ClientTask
.find({ taskId: query.taskId })
.populate('clientId', { name: 1 }).exec(function (err, clientTask) {
if (!clientTask) {
res.status(404).json({ message: 'Client task not found' })
}
// your logic
});

You can fetch aggregated data with mongodb aggregate. To Calculates aggregate values for the data in a collection:
$lookup Performs a left outer join to an unsharded collection in the same database to filter in documents from the “joined” collection for processing. To each input document, the $lookup stage adds a new array field whose elements are the matching documents from the “joined” collection. The $lookup stage passes these reshaped documents to the next stage.
In your case:
Model.ClientTask.aggregate([
{
$lookup:
{
from: "client",
localField: "_id",
foreignField: "clientId",
as: "clientData"
},
},
{
$project: {
"name": clientData.name, // This name will be client name from client collections
"taskId": 1,
"clientTaskId": 1,
"active": 1
}
}
],
function (err, response) {
console.log(err, response)
});

Related

Nodejs mongodb API performance

I want to create an API which will query users in my mongodb and returns the data of all users. For each user i need to perform additional query to get count in 2 other schema as below:
const getUsersSummary = async (req, res, next) => {
try {
const users = await User.fetch();
const usersWithCount = await Promise.all(
users.map(async (user) => {
let tests;
let enrollments;
try {
tests = await Test.countDocuments(
{ user: user._id }
);
enrollments = await Enrollment.countDocuments(
{ user: user._id }
);
} catch (e) {
tests = 0;
enrollments = 0;
}
return {
_id: user._id,
name: user.name,
address: user.address,
tests: tests,
enrollments: enrollments,
};
})
);
return res.json({
users: usersWithCount,
});
} catch (err) {
next(err);
}
};
I want to know if this is a good way to do it.
The code works. But i am concerned about the performance and load it will put on my server.
I think you might consider use the MongoDB aggregation framework. You can combine user data with trial and registration data using the $lookup operator to join the user data with the tests and enrollments data, and then use the $group operator to get the count of tests and enrollments for each user. This will reduce the number of database queries and improve the performance of your API.
const aggregate = User.aggregate([
{
$lookup: {
from: "tests",
localField: "_id",
foreignField: "user",
as: "tests"
}
},
{
$lookup: {
from: "enrollments",
localField: "_id",
foreignField: "user",
as: "enrollments"
}
},
{
$project: {
_id: 1,
name: 1,
address: 1,
tests: { $size: "$tests" },
enrollments: { $size: "$enrollments" }
}
}
]);
const usersWithCount = await aggregate.exec();
return res.json({
users: usersWithCount,
});

Mongo Find is not working

This is my user schema
var UserSchema = new Schema({
Pcard: [{ type: Schema.Types.ObjectId, ref: 'Pcard' }]
})
These are id's saved in user Pcard array
"user": { // id 59560bc83e1fdc2cb8e73236
"Pcard": [
"595b43d16e4b7305e5b40845",
"595b459a6e4b7305e5b40848",
"595f48f58117c85e041f1e1c",
],
}
This is my Pcard Scema
var PcardSchema = new Schema({
Time : {
type : Date,
default: Date.now
},
})
I want to find user having Id and which also contains some id in Pcard array
User.find({ _id: req.user._id,
Pcard:{$in : [req.params.PcardId] }
}, function (err, Userpresent) {
if (err) {
res.json(code.Parked);
}
if (Userpresent === null || Userpresent === undefined) {
res.json(code.notAllowed);
}else{
This else is execting everytime.
}
}
});
when i querying with user which does not have a Pcardid in Pcard array it is still going in else condition !
for eg . i am querying with this id 59560bc83e1fdc2cb8e73236 and this not contain 5957bd177e996b56d08b991a in Pcard array but still it is going on else part of the user query.
If you are expecting only one user, use findOne.
Might be useful/cleaner for you to return early instead of having a bunch if-else.
User.findOne({
_id: req.user._id,
Pcard: { $in : [req.params.PcardId] }
}, function (err, user) {
if (err) {
return res.json(code.Parked);
}
// simplified: undefined and null are both falseys
if (!user) {
return res.json(code.notAllowed);
}
// user should be found at this point
});
Good read: All falsey values in JavaScript

How to join two collections in mongoose

I have two Schema defined as below:
var WorksnapsTimeEntry = BaseSchema.extend({
student: {
type: Schema.ObjectId,
ref: 'Student'
},
timeEntries: {
type: Object
}
});
var StudentSchema = BaseSchema.extend({
firstName: {
type: String,
trim: true,
default: ''
// validate: [validateLocalStrategyProperty, 'Please fill in your first name']
},
lastName: {
type: String,
trim: true,
default: ''
// validate: [validateLocalStrategyProperty, 'Please fill in your last name']
},
displayName: {
type: String,
trim: true
},
municipality: {
type: String
}
});
And I would like to loop thru each student and show it's time entries. So far I have this code which is obviously not right as I still dont know how do I join WorksnapTimeEntry schema table.
Student.find({ status: 'student' })
.populate('student')
.exec(function (err, students) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
}
_.forEach(students, function (student) {
// show student with his time entries....
});
res.json(students);
});
Any one knows how do I achieve such thing?
As of version 3.2, you can use $lookup in aggregation pipeline to perform left outer join.
Student.aggregate([{
$lookup: {
from: "worksnapsTimeEntries", // collection name in db
localField: "_id",
foreignField: "student",
as: "worksnapsTimeEntries"
}
}]).exec(function(err, students) {
// students contain WorksnapsTimeEntries
});
You don't want .populate() here but instead you want two queries, where the first matches the Student objects to get the _id values, and the second will use $in to match the respective WorksnapsTimeEntry items for those "students".
Using async.waterfall just to avoid some indentation creep:
async.waterfall(
[
function(callback) {
Student.find({ "status": "student" },{ "_id": 1 },callback);
},
function(students,callback) {
WorksnapsTimeEntry.find({
"student": { "$in": students.map(function(el) {
return el._id
})
},callback);
}
],
function(err,results) {
if (err) {
// do something
} else {
// results are the matching entries
}
}
)
If you really must, then you can .populate("student") on the second query to get populated items from the other table.
The reverse case is to query on WorksnapsTimeEntry and return "everything", then filter out any null results from .populate() with a "match" query option:
WorksnapsTimeEntry.find().populate({
"path": "student",
"match": { "status": "student" }
}).exec(function(err,entries) {
// Now client side filter un-matched results
entries = entries.filter(function(entry) {
return entry.student != null;
});
// Anything not populated by the query condition is now removed
});
So that is not a desirable action, since the "database" is not filtering what is likely the bulk of results.
Unless you have a good reason not to do so, then you probably "should" be "embedding" the data instead. That way the properties like "status" are already available on the collection and additional queries are not required.
If you are using a NoSQL solution like MongoDB you should be embracing it's concepts, rather than sticking to relational design principles. If you are consistently modelling relationally, then you might as well use a relational database, since you won't be getting any benefit from the solution that has other ways to handle that.
It is late but will help many developers.
Verified with
"mongodb": "^3.6.2",
"mongoose": "^5.10.8",
Join two collections in mongoose
ProductModel.find({} , (err,records)=>{
if(records)
//reurn records
else
// throw new Error('xyz')
})
.populate('category','name') //select only category name joined collection
//.populate('category') // Select all detail
.skip(0).limit(20)
//.sort(createdAt : '-1')
.exec()
ProductModel Schema
const CustomSchema = new Schema({
category:{
type: Schema.ObjectId,
ref: 'Category'
},
...
}, {timestamps:true}, {collection: 'products'});
module.exports = model('Product',CustomSchema)
Category model schema
const CustomSchema = new Schema({
name: { type: String, required:true },
...
}, {collection: 'categories'});
module.exports = model('Category',CustomSchema)

How to use populate functionality by using populate or making inner query with aggregation in mongodb

I have following data in my Mongodb.
{
"_id" : ObjectId("54a0d4c5bffabd6a179834eb"),
"is_afternoon_scheduled" : true,
"employee_id" : ObjectId("546f0a06c7555ae310ae925a")
}
I would like to use populate with aggregate, and want to fetch employee complete information in the same response, I need help in this. My code is:
var mongoose = require("mongoose");
var empid = mongoose.Types.ObjectId("54a0d4c5bffabd6a179834eb");
Availability.aggregate()
.match( { employee_id : empid} )
.group({_id : "$employee_id",count: { $sum: 1 }})
.exec(function (err, response) {
if (err) console.log(err);
res.json({"message": "success", "data": response, "status_code": "200"});
}
);
The response i am getting is
{"message":"success","data":{"_id":"54a0d4c5bffabd6a179834eb","count":1},"status_code":"200"}
My expected response is:
{"message":"success","data":[{"_id":"54aa34fb09dc5a54232e44b0","count":1, "employee":{fname:abc,lname:abcl}}],"status_code":"200"}
You can call the model form of .populate() on the result objects from an aggregate operation. But the thing is you are going to need a model to represent the "Result" object returned by your aggregation in order to do so.
There are a couple of steps, best explained with a complete listing:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
var employeeSchema = new Schema({
"fname": String,
"lname": String
})
var availSchema = new Schema({
"is_afternoon_scheduled": Boolean,
"employee_id": {
"type": Schema.Types.ObjectId,
"ref": "Employee"
}
});
var resultSchema = new Schema({
"_id": {
"type": Schema.Types.ObjectId,
"ref": "Employee"
},
"count": Number
});
var Employee = mongoose.model( "Employee", employeeSchema );
var Availability = mongoose.model( "Availability", availSchema );
var Result = mongoose.model( "Result", resultSchema, null );
mongoose.connect('mongodb://localhost/aggtest');
async.series(
[
function(callback) {
async.each([Employee,Availability],function(model,callback) {
model.remove({},function(err,count) {
console.log( count );
callback(err);
});
},callback);
},
function(callback) {
async.waterfall(
[
function(callback) {
var employee = new Employee({
"fname": "abc",
"lname": "xyz"
});
employee.save(function(err,employee) {
console.log(employee),
callback(err,employee);
});
},
function(employee,callback) {
var avail = new Availability({
"is_afternoon_scheduled": true,
"employee_id": employee
});
avail.save(function(err,avail) {
console.log(avail);
callback(err);
});
}
],
callback
);
},
function(callback) {
Availability.aggregate(
[
{ "$group": {
"_id": "$employee_id",
"count": { "$sum": 1 }
}}
],
function(err,results) {
results = results.map(function(result) {
return new Result( result );
});
Employee.populate(results,{ "path": "_id" },function(err,results) {
console.log(results);
callback(err);
});
}
);
}
],
function(err,result) {
if (err) throw err;
mongoose.disconnect();
}
);
That's the complete example, but taking a closer look at what happens inside the aggregate result is the main point:
function(err,results) {
results = results.map(function(result) {
return new Result( result );
});
Employee.populate(results,{ "path": "_id" },function(err,results) {
console.log(results);
callback(err);
});
}
The first thing to be aware of is that the results returned by .aggregate() are not mongoose documents as they would be in a .find() query. This is because aggregation pipelines typically alter the document in results from what the original schema looked like. Since it is just a raw object, each element is re-cast as a mongoose document for the Result model type defined earlier.
Now in order to .populate() with data from Employee, the model form of this method is called on the array of results in document object form along with the "path" argument to the field to be populated.
The end result fills is the data as it comes from the Employee model it was related to.
[ { _id:
{ _id: 54ab2e3328f21063640cf446,
fname: 'abc',
lname: 'xyz',
__v: 0 },
count: 1 } ]
Different to how you process with find, but it is necessary to "re-cast" and manually call in this way due to how the results are returned.
This is working like applied populate with aggregate using inner query.
var mongoose = require("mongoose");
var empid = mongoose.Types.ObjectId("54a0d4c5bffabd6a179834eb");
Availability.aggregate()
.match( { employee_id : empid} )
.group({_id : "$employee_id",count: { $sum: 1 }})
.exec(function (err, response) {
if (err) console.log(err);
if (response.length) {
var x = 0;
for (var i=0; i< response.length; i++) {
empID = response[i]._id;
if (x === response.length -1 ) {
User.find({_id: empID}, function(err, users){
res.json({"message": "success", "data": users, "status_code": "200"});
});
}
x++;
}
}
}
);

Get result as an array instead of documents in mongodb for an attribute

I have a User collection with schema
{
name: String,
books: [
id: { type: Schema.Types.ObjectId, ref: 'Book' } ,
name: String
]
}
Is it possible to get an array of book ids instead of object?
something like:
["53eb797a63ff0e8229b4aca1", "53eb797a63ff0e8229b4aca2", "53eb797a63ff0e8229b4aca3"]
Or
{ids: ["53eb797a63ff0e8229b4aca1", "53eb797a63ff0e8229b4aca2", "53eb797a63ff0e8229b4aca3"]}
and not
{
_id: ObjectId("53eb79d863ff0e8229b97448"),
books:[
{"id" : ObjectId("53eb797a63ff0e8229b4aca1") },
{ "id" : ObjectId("53eb797a63ff0e8229b4acac") },
{ "id" : ObjectId("53eb797a63ff0e8229b4acad") }
]
}
Currently I am doing
User.findOne({}, {"books.id":1} ,function(err, result){
var bookIds = [];
result.books.forEach(function(book){
bookIds.push(book.id);
});
});
Is there any better way?
It could be easily done with Aggregation Pipeline, using $unwind and $group.
db.users.aggregate({
$unwind: '$books'
}, {
$group: {
_id: 'books',
ids: { $addToSet: '$books.id' }
}
})
the same operation using mongoose Model.aggregate() method:
User.aggregate().unwind('$books').group(
_id: 'books',
ids: { $addToSet: '$books.id' }
}).exec(function(err, res) {
// use res[0].ids
})
Note that books here is not a mongoose document, but a plain js object.
You can also add $match to select some part of users collection to run this aggregation query on.
For example, you may select only one particular user:
User.aggregate().match({
_id: uid
}).unwind('$books').group(
_id: 'books',
ids: { $addToSet: '$books.id' }
}).exec(function(err, res) {
// use res[0].ids
})
But if you're not interested in aggregating books from different users into single array, it's best to do it without using $group and $unwind:
User.aggregate().match({
_id: uid
}).project({
_id: 0,
ids: '$books.id'
}).exec(function(err, users) {
// use users[0].ids
})

Resources