I am a nodejs beginner and I ran into some callback behaviour that I don't understand. I am using an Express router to write Mongoose objects to Mongodb using a POST request. In the body of the request I am passing in a nested json structure with two fields - jobDetails and examples. The data in jobDetails is used to create a Job Mongoose object and the data in examples is used to create several Example Mongoose objects. The Job and Example objects are linked in that a Job contains a list of Example objects in one of it's fields.
The way I tried to implement this was with callbacks in the following way. Basically I save the Job object to mongo first, then iterate over the examples - each time creating an Example object and linking it to the job via the .job field and also saving the Example object to mongo. Then in the callback to the Example object save function I updated the Job object with the new Example object and saved the updated version to mongo.
router.post('/jobs', function (req, res, next) {
var job = new Job(req.body.jobDetails);
var examples = req.body.examples;
console.log("JOB DETAILS");
console.log(req.body.jobDetails);
console.log("EXAMPLES");
console.log(req.body.examples);
//save job
job.save(function (err, job) {
console.log(err);
});
//save examples
for(i=0; i<examples.length;i++){
var eg = new Example({content: examples[i]});
eg.job=job;
eg.save(function (err, eg){
job.examples.push(eg);
job.save(function(err, job){
console.log(err);
});
console.log(err);
});
}
});
This did not perform as I would expect. Specifically, double the number of examples were actually saved to mongo with several duplicates and some missing. I understand that callbacks are asynchronous but to me that still doesn't explain why double the number of examples would be saved and some would be duplicated and some would be missing.
I eventually got it to work correctly without using callbacks at all in the following way.
router.post('/jobs', function (req, res, next) {
var job = new Job(req.body.jobDetails);
var examples = req.body.examples;
console.log("JOB DETAILS");
console.log(req.body.jobDetails);
console.log("EXAMPLES");
console.log(req.body.examples);
//save job
job.save(function (err, job) {
console.log(err);
});
//save examples
for(i=0; i<examples.length;i++){
var eg = new Example({content: examples[i]});
eg.job=job;
eg.save(function (err, eg){
console.log(err);
});
job.examples.push(eg);
job.save(function(err,job){
console.log(err);
});
}
});
And I'm not sure if this is the optimal solution either. But I would like to know why my original approach resulted in the unintended behaviour.
This should work..
router.post('/jobs', function(req, res, next) {
var job = new Job(req.body.jobDetails);
var examples = req.body.examples;
console.log("JOB DETAILS");
console.log(req.body.jobDetails);
console.log("EXAMPLES");
console.log(req.body.examples);
//save job
job.save(function(err, result) {
if (!err) {
//save examples
for (i = 0; i < examples.length; i++) {
var eg = new Example({
content: examples[i]
});
eg.job = job;
eg.save(function(err, eg) {
job.examples.push(eg);
job.save(function(err, job) {
if (!err)
job.examples = [];
});
console.log(err);
});
}
}
});
});
I would suggest you use a library like async to perform such save operations in a step by step manner. Follow this approach for better readability of code and better results
var async = require('async');
router.post('/jobs', function(req, res, next) {
var job = new Job(req.body.jobDetails);
var examples = req.body.examples;
var savedExamples = [];
console.log("JOB DETAILS");
console.log(req.body.jobDetails);
console.log("EXAMPLES");
console.log(req.body.examples);
async.eachSeries(examples, function iteratee(example, callback) {
var eg = new Example({
content: example
});
eg.job = job;
eg.save(function(err, savedEg) {
if(!err) {
savedExamples.push(savedEg);
}
callback(err)
});
}, function(err) {
if(err) {
//handle errors
}
job.examples = savedExamples;
job.save(function(err,job) {
if(err) {
//handle errors
}
//success callback
});
});
});
Using this approach you will have to call the save function for job only once after all other operations are completed. If an error is triggered at any point the whole flow is stopped. For more info regarding async library refer this!
Related
I have an api server and some script jobs. They are using the same function to pull a roster using mongoose and populate the players in the roster.
On the api server, this function is called normally. Using the script, it doesn't.
API example
function getRoster(id) {
var deferred = Q.defer();
Roster.find({_id:id}, 'playerRoster userId tournamentId').populate('playerRoster').exec(
function(err, roster) {
if (err) {
deferred.resolve(err);
}
deferred.resolve(roster[0]);
});
return deferred.promise;
}
api.post('/get_roster', function(req, res) {
// get tournament
var id = req.body._id;
var playerId = req.body.playerId;
getRoster(id).then(function(data) {
var roster=data;
res.json(roster);
});
});
Script
module.exports = function(config) {
this.getRoster=function(id) {
//return Roster.find({_id:id}, 'playerRoster userId tournamentId').exec( - THIS RETURNS
return Roster.find({_id:id}, 'playerRoster userId tournamentId').populate('playerRoster').exec(
function(err, roster) {
if (err) {
return err;
}
console.log('roster[0]',roster);
return roster[0];
});
}
this.tallyPoints = function(tournamentPlayer,sportsPlayers) {
var deferred = Q.defer();
var totalPoints =0;
console.log("tallyPoints 0 ",tournamentPlayer);
var rosterId = tournamentPlayer.player.roster[0];
console.log("tallyPoints 1 ",rosterId);
this.getRoster(rosterId).then(function(roster2){
console.log("tallyPoints 2 ",roster2);
...
deferred.resolve(totalPoints);
});
return deferred.promise;
}
return this;
};
In the script, neither logging for the roster[0] or tallyPoints 2 lines print, but there is no error either.
Why doesn't Roster.find return when I add populate? The only thing I can imagine is because playerRoster collection has 2000 records searching for ~10 and it hits some timeout that isn't being caught.
Any suggestion to clean it up is also appreciated.
Thanks
Moongoose supports promises for a long time. It's unsuitable to use callback-based Mongoose API where promises are desirable and the use of Q.defer with existing promises is known as Deferred antipattern (similarly, new Promise results in promise construction antipattern).
In its current state getRoster doesn't return a promise and doesn't handle errors correctly.
function getRoster(id) {
return Roster.find({_id:id}, 'playerRoster userId tournamentId').populate('playerRoster').exec()
.then(roster => roster[0]);
}
api.post('/get_roster', function(req, res) {
// get tournament
var id = req.body._id;
var playerId = req.body.playerId;
getRoster(id)
.then(function(data) {
var roster=data;
res.json(roster);
})
.catch(err => {
// handle error
});
});
Considering that only roster[0] is used, it likely should be changed to Roster.findOne.
It doesn't matter whether getRoster is used in Express route or elsewhere, it should work. It's unknown how module.exports = function(config) {...} module is used, but this may refer to wrong context if it isn't used as class. If getRoster and tallyPoints don't use config, they shouldn't reside inside this function.
I have a big problem.
I want to iterate over collection a result set and for each set i want to find one result.
This looks like this:
router.get('/', function(req, res) {
var floors = [];
var rooms = [];
req.db.collection('floors').find().sort({_id: 1}).forEach(function(floor) {
floors.push(floor);
});
req.db.collection('rooms').find().sort({_id: 1}).forEach(function(room) {
req.db.collection('floors').findOne({_id: new ObjectID(room.floorId)}, function(error, floor) {
room.floor = floor;
rooms.push(room);
});
});
res.render('rooms', { floors: floors, rooms: rooms });
});
The Problem is that the page will be rendered before the iteration is complete.
I tried to use async and promises, but i didn't get it to run.
Basically you have to wait until all your queries are done before sending the rendering result. Unfortunately you don't use promises so this will get a bit messy.
It appears that you are using the native client and according to the docs there is a second callback that gets called when all iterations are done
http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#forEach
router.get('/', function(req, res, next) {
var floors = [];
var rooms = [];
function done(err){
if(err) {
return next(err);
}
res.render('rooms', { floors: floors, rooms: rooms });
}
function getRooms(err){
if(err){
return next(err);
}
req.db.collection('rooms').find().sort({_id: 1}).forEach(function(room) {
// you already have all the floors, no need to hit the db again
floors.find(floor => floor._id === room.floorId); // not sure about this 100% as _id could be an object
}, done);
}
req.db.collection('floors').find().sort({_id: 1}).forEach(function(floor) {
floors.push(floor);
}, getRooms);
});
to be noted that this request will get quite heavy when your db grows.
I'm making a CRUD List App using Mongo/Express and having callback trouble!
I have a routes file with my HTTP methods and a services file that pulls in my mongoose model, Item.
Because this is from a class I'm taking I already have a GET and POST that are running correctly, but having a total headache trying to get my DELETE working.
Routes.js
router.post('/items', function(req, res) {
Item.save(req.body.name, function(item) {
res.status(201).json(item);
}, function(err) {
res.status(400).json(err);
});
});
router.delete('/items/:id'), function(req, res) {
var ref = req.params.id;
Item.remove(ref, function(err, item){
res.json(item);
})
};
Services.js
exports.save = function(name, callback, errback) {
Item.create({ name: name }, function(err, item) {
if (err) {
errback(err);
return;
}
callback(item);
});
};
exports.remove = function(id, callback){
Item.findByIdAndRemove(id, callback())
};
Obviously my delete/remove is still pretty CRUD.... Included the POST for reference. Help would be amazing as attempts at translating existing examples into my architecture have been total fails!
Your punctual problem is that you execute the callback instead of passing it into the mongoose method in the remove function:
exports.remove = function(id, callback){
Item.findByIdAndRemove(id, callback())
};
should be, so that mongoose can call it:
exports.remove = function(id, callback){
Item.findByIdAndRemove(id, callback)
};
I wonder why you feel it necessary to wrap the the mongoose methods findByIdAndRemove, and create with your own and to put them in a service? The new methods don't seem to add any value.
If you want to do things before or after deleting the model, it would be more idiomatic to trap the mongoose model life-cycle methods using middleware: http://mongoosejs.com/docs/middleware.html.
I have following code to fetch some data from the db (mongo).
function getAllUsers(){
var UsersPromise = Q.defer();
UserSchema.find({}, function(err, data){
if(err){
UsersPromise .reject(err);
}else {
UsersPromise .resolve(data);
}
});
return UsersPromise .promise;
}
Then I modify each of these models. I add certain fields to the model depending on the type of user. (This is working correctly).
function buildUsers(users){
// my code iterates over users and adds
// properties as required.
// Working fine.
return users; // updated users.
}
Now I want to save these updated models back to mongo and this is where it's making me pull my hair.
function saveUsers(users){
// here, the users are received correctly. But the following line to save the users fails.
var SaveUsersPromise = Q.defer();
UserSchema.save(users, function(err, data){
if(err){
SaveUsersPromise .reject(err);
} else {
SaveUsersPromise .resolve(data);
}
});
return SaveUsersPromise .promise;
}
Lastly I call these functions like:
DB.connect()
.then(getAllUsers)
.then(buildUsers)
.then(saveUsers)
.catch(errorHandler);
Everything works correctly untill I call UserSchema.save. What could be the problem?
PS: I am using mongoose.
TIA.
UserSchema.save accepts single instance, you have to loop through users and save each. Mongoose doesn't have bulk inserts implemented yet (see issue #723).
Here's simple implementation using async.eachSeries
function saveUsers(users){
var async = require('async'); // <== npm install async --save
var SaveUsersPromise = Q.defer();
async.eachSeries(users, function(user, done){
UserSchema.save(user, done);
// or
user.save(done); // if user is Mongoose-document object
}, function(err){
if(err){
SaveUsersPromise.reject(err);
} else {
SaveUsersPromise.resolve();
}
});
return SaveUsersPromise.promise;
}
I have a dashboard view ( dashboard.jade ) that will display two panels with different information, all that info should be retrieved from a database and then sent to the view.
Let's say i have a route file ( document.js ) with two actions defined:
exports.getAllDocuments = function(req, res){
doc = db.model('documents', docSchema);
doc.find({}, function(err, documents) {
if (!err) {
// handle success
}
else {
throw err;
}
});
};
exports.getLatestDocumentTags = function(req, res){
tags = db.model('tags', tagSchema);
tags.find({}, function(err, docs) {
if (!err) {
// handle success
}
else {
throw err;
}
});
};
These functions would only serve the porpuse of retrieving data from the database.
Now i would like to send that data to the dashboard view from my dashboard.js route file under exports.index function where i render my dashboard view.
The problem is, since the db calls will be async i wouldn't have access to the data before i could call the view.
I guess i could have an action that simply did all my db calls and through callbacks deliver all the data at once to the view but that would make my data retrieval actions not reusable.
I'm really confused on how to tackle this problem correctly, probably i'm getting this async thing all wrong. Can someone give me some hints on how to do this properly ?
Here's something to pique your interest.
//Check out the async.js library
var async = require('async');
//Set up your models once at program startup, not on each request
//Ideall these would be in separate modules as wel
var Doc = db.model('documents', docSchema);
var Tags = db.model('tags', tagSchema);
function index(req, res, next) {
async.parallel({ //Run every function in this object in parallel
allDocs: async.apply(Doc.find, {}) //gets all documents. async.apply will
//do the equivalent of Doc.find({}, callback) here
latestDocs: async.apply(Tags.find, {})
], function (error, results) { //This function gets called when all parallel jobs are done
//results will be like {
// allDocs: [doc1, doc2]
// latestDocs: [doc3, doc4]
// }
res.render('index', results);
});
}
exports.index = index;
};
Try some more tutorials. If you haven't had the "a ha" moment about how async programming works in node, keep going through guided, hand-held tutorials before trying to write brand new programs without guidance.
//Check out the async.js library and mangoose model
var mongoOp = require("./models/mongo");
var async = require('async');
router.get("/",function(req,res){
var locals = {};
var userId = req.params.userId;
async.parallel([
//Load user Data
function(callback) {
mongoOp.User.find({},function(err,user){
if (err) return callback(err);
locals.user = user;
callback();
});
},
//Load posts Data
function(callback) {
mongoOp.Post.find({},function(err,posts){
if (err) return callback(err);
locals.posts = posts;
callback();
});
}
], function(err) { //This function gets called after the two tasks have called their "task callbacks"
if (err) return next(err); //If an error occurred, we let express handle it by calling the `next` function
//Here `locals` will be an object with `user` and `posts` keys
//Example: `locals = {user: ..., posts: [...]}`
res.render('index.ejs', {userdata: locals.user,postdata: locals.posts})
});