I've a Parent and Child models in my app.
Parent.create receives parent_name and an array of children that I want to add to the Parent model, the following flow describes the function:
1) Create parent object
2) Create all children
3) Save parent with updated children array
The problem is that Parent.create is probably async, and the 'created_children' array when saved to parent is empty (because it doesn't wait until the Parent.create finishes.
How can I make Model.create dependent (or synchronic)?
See the code below (I commented the buggy part //BUG: EMPTY ARRAY!!!!!!!!!!):
create: function(req, res, next) {
var childrenInput = req.param('children');
var parentObj = {
name: req.param('parent_name')
};
Parent.create(parentObj, function parentCreated(err, parent) {
if (err) {
return res.redirect('/parent/new');
}
// assign children
var created_children = new Array();
for(var i=0; i < childrenInput.length; i++) {
var childObj = {
name: parentObj.childrenInput[i],
parent_id: parent.id
};
// create child
Child.create(childObj, function childCreated(err, child) {
if (err) {
for(var j=0; j < created_children.length; j++) {
Child.destroy(created_children[j].id, function childDestroyed(err) {
if (err)
{
// BIG ERROR
return next(err);
}
});
}
return res.redirect('/parent/new');
}
// add created child
created_children.push(child.id);
}) // end of Child.create;
} // end of for;
// save created children to parent
parent.children = created_children.slice();
parent.save(function(err, c) {
if (err)
{
// TODO: FUNCTION TO DESTROY ALL CHILDREN
return next(err);
}
});
return res.redirect('/parent/show/' + parent.id);
});
},
Parent model
module.exports = {
schema: true,
attributes: {
name: {
type: 'string',
required: true,
unique: true
},
children: {
type: 'array',
defaultsTo: []
}
}
};
Performing asynchronous operations on an array can be a real pain. I'd suggest using a module like async, which provides synchronous-like functionality for asynchronous code. You could then rewrite your code as:
Parent.create(parentObj, function parentCreated(err, parent) {
if (err) {
return res.redirect('/parent/new');
}
// You only really need this for error handling...
var created_children_ids = new Array();
// Create an array of child instances from the array of child data
async.map(
// Array to iterate over
childrenInput,
// Iterator function
function(childObj, callback) {
Child.create(childObj, function childCreated(err, child) {
if (err) {return callback(err);}
created_children_ids.push(child.id);
// 'null' indicates no error
return callback(null, child);
});
},
// Callback for when loop is finished.
// If any run of the iterator function resulted in the
// callback being called with an error, it will immediately
// exit the loop and call this function. Otherwise the function
// is called when the loop is finished, and "results" contains
// the result of the mapping operation
function (err, results) {
if (err) {return destroyChildren();}
// Save the children to the parent
parent.children = results;
parent.save(function(err, c) {
if (err) {return destroyChildren();}
return res.redirect('/parent/show/' + parent.id);
});
function destroyChildren(err) {
Child.destroy({id: created_children_ids}).exec(function() {
// Respond with an error
return res.serverError(err);
});
}
}
);
});
Note that if you're using Sails v0.10, you can use actual associations to bind the parent and child records, and use parent.children.add(childObj) (which is a synchronous operation) in a regular loop prior to calling parent.save(). Calling .add with an object will cause that model to be created during the save operation.
Related
I have a large aggrogate query that required me to pass "allowDiskUse: true" as an option. This would not work with the aggegate as described here:
https://github.com/meteorhacks/meteor-aggregate/issues/11
My meteor method is defined here. When I call the method I need to wait for ondata to complete before anything is returned to the client, but nothing I try allows me to get that data in a safe way up to the front end.
Meteor.methods({
'getSummary': function (dept,startDate,endDate,filterType) {
f = myQuery(startdate,enddate,dayFinalGroup);
f.on("data", Meteor.bindEnvironment(function(row) {
//load an array or something here to return
}));
f.once("end", Meteor.bindEnvironment(function() {
// tidy up, in my case end the stream
}));
//here I'd return the array loaded
},
});
This is my front end.
Meteor.call(
'getSummary',0,Session.get('start_date'),Session.get('end_date'),1,
function(error, result){
if(error){
console.log(error);
} else {
Session.set('sumTotals',result);
}
}
);
Finally Got it. I utilized wrapSync
'getSummary': function (dept,startDate,endDate,filterType) {
console.log(dept);
console.log(startDate);
console.log(endDate);
console.log(filterType);
var startdate = new Date(startDate);
var enddate = new Date(endDate);
var arr = [];
f = myQuery(startdate,enddate,dayFinalGroup);
var fetchCursor = Meteor.wrapAsync(function fetchCursor (cursor, cb) {
cursor.each(function (err, doc) {
if (err) return cb(err);
if (!doc) return cb(null, { done: true }); // no more documents
arr.push(doc);
});
});
var myData = fetchCursor(f);
return arr;
I need to insert records in two collections. The second collection stores the ID of records of the first collection. It is a 1:m (fisrt:second) situation. The trigger is the second collection:
If a record for the second collection needs to be stored
check if there is already a fitting record in the first collection
if not: then save one in the first collection
store the second collection
save the id of the record of the first collection in the second collection
The following example seems to fullfill these steps. But I have the promises only half way.
How can this be done in a better "promised" way?
saveObjects(name: String, objects: Array<IObject>){
var promise = FirstModel.findOne({Name : name}).exec();
promise.then(function(res1){
if (!res1){
var la = new FirstModel();
la.Name = name;
la.save(function(err){
if (err) throw err;
})
}
}).error(function(err){
throw err;
})
objects.forEach(function(obj) {
FirstModel.findOne({Name : name},'_id',function(err, res2){
if (err) throw err;
var vo = new SecondModel();
vo.Name = name;
vo.FistID = res2._id;
vo.save(function(err){
if (err) throw err;
});
});
});
}
I'm gona assume here that you're using bluebird or another promise equivalent set up.
var Promise = require('bluebird');
saveObjects(name: String, objects: Array < IObject > ) {
// Get the first model
return FirstModel
.findOne({
Name: name
})
.exec()
.then(function (res1) {
if (!res1) {
var la = new FirstModel();
la.Name = name;
return la.save() // We return the save operation on the model, save() returns a promise
}
// We just return the model normally so it passes down the chain.
return res1;
})
.then(function (res1) {
// Here we use Promise.all() method on bluebird which accepts an Array
// of promises, we create a promise from the objects array by using Array.map() which
// goes through every object in the array and creates a new array.
return Promise
.all(objects.map(function (obj) {
// We go through each object in objects and create a new
// model and return the save() method of each model, this
// creates a array of promises which are resolved when each
// all model has been saved.
var vo = new SecondModel();
vo.Name = name;
vo.FistID = res1._id;
return vo.save();
}));
})
.then(function (models) {
// here we have all the models we just saved in an array called models,
// do what you want with it.
})
.error(function (err) {
throw err;
})
}
For more info see documentation on Array.map() here and Promise.all() here
I created a service called AppService.
Its function getUserPostionOptions is supposed to return an object:
getUserPostionOptions: function (user) {
// PositionOptions.findOne({id:'53f218deed17760200778cfe'}).exec(function (err, positionOptions) {
var positionDirectionsOptions = [1,2,3];
var positionLengthsOptions = [4,5,6];
var object = {
directions:positionDirectionsOptions,
lengths:positionLengthsOptions
};
return object;
// });
}
This works, in my controller positionOptions gets populated correctly:
var positionOptions = AppService.getUserPostionOptions(user);
However, when I uncomment the find query the item is found but the object returns undefined.
Thank in advance for your help
SailsJs ORM (and almost NodeJs database querying methods) uses non-blocking mechanism via callback function. So you have to change your code into:
getUserPostionOptions: function (user, callback) {
PositionOptions.findOne({id:'53f218deed17760200778cfe'}).exec(function (err, positionOptions) {
var positionDirectionsOptions = [1,2,3];
var positionLengthsOptions = [4,5,6];
var object = {
directions:positionDirectionsOptions,
lengths:positionLengthsOptions
};
callback(null, object); // null indicates that your method has no error
});
}
Then just use it:
AppService.getUserPostionOptions(user, function(err, options) {
if (!err) {
sails.log.info("Here is your received data:");
sails.log.info(options);
}
});
Suppose I have 2 models (one-to-many); Center model (one) - Room model (many).
When creating a center, an array of rooms is created in the Center.Create callback.
If a room creation fails, it should destroy all the created data before the failed room entity.
CenterController create:
create: function(req, res) {
console.log('params: ', req.params.all());
var centerObj = {
name: req.param('center_name'),
state: req.param('center_state')
};
var roomsInput = req.params('rooms');
console.log('created center centerObj: ', centerObj);
Center.create(centerObj, function centerCreated(err, center) {
if (err) {
console.log(err);
req.session.flash = {
err: err
}
console.log("Error in center create")
return res.redirect('/center/new');
}
// keep track of successfully created rooms
var created_rooms_ids = new Array();
async.mapSeries(
// array to iterate over
roomsInput,
// iterator function
function(roomInput, cb)
{
var roomObj = {
name: roomInput.name,
center: center.id,
min_age: roomInput.min_age,
max_age: roomInput.max_age
};
Room.create(roomObj, function roomCreated(err, room) {
if (err) {
console.log("Room.create error: ", err);
return cb(err);
}
created_rooms_ids.push(room.id);
return cb(null, room.id);
});
},
// callback for when the loop is finished
function(err, results)
{
if (err) {
console.log('error');
return destroyCreatedResources(err);
}
console.log('center: ', center);
return res.redirect('/center/show/' + center.id);
// destroy created resources (center + room)
function destroyCreatedResources(err)
{
console.log("destroyCreatedResources. Center=", center.id, "Id=", created_rooms_ids);
Center.destroy({id: center.id}).exec(function(e){
Room.destroy({id: created_rooms_ids}).exec(function(e){
console.log('Room.destroy');
if (e) {console.log('Room.destroy error!!!!!!!!!!!!!!!!!!!!!!');}
return res.serverError(err);
});
});
}
}
);
});
},
Problem
When an error happens in the middle and I want to perform reverse destruction of all the created rooms, only the center is destroyed.
How come res.serverError(err); is called before the Rooms are destroyed?
function destroyCreatedResources(err)
{
Center.destroy({id: center.id}).exec(function(e){
console.log('Room.destroy');
if (e) {console.log('Room.destroy error!!!!!!!!!!!!!!!!!!!!!!');}
Room.destroy({id: created_rooms_ids}).exec(function(e){
return res.serverError(err);
});
});
}
Are there better ways to do reverse destruction?
Looks like this was due to a bug in sails-mongo v0.10.0-rc2. This has now been patched and released as v0.10.0-rc3, so you can pull down the latest from npm and the issue should be resolved. Thanks!
How do I manage batch save in Mongoose? I saw it may not be possible yet:
How can I save multiple documents concurrently in Mongoose/Node.js?
Theres some mention about using some flow control library like q, but I also notice there promises in mongoose, can it be used? Can I do like in jQuery Deferred/Promises
$.when(obj1.save(), obj2.save(), obj3.save()).then ->
# do something?
Yes, you can do this with promises. If you were using the Q promise library, you could re-write #matz3's code like:
var tasks = [];
for (var i=0; i < docs.length; i++) {
tasks.push(docs[i].save());
}
Q.all(tasks)
.then(function(results) {
console.log(results);
}, function (err) {
console.log(err);
});
We start all the operations one at a time in the loop, but we don't wait for any of them to complete, so they run in parallel. We add a promise (that acts like a placeholder for the result) to an array. We then wait for all the promises in the array of promises to complete.
Most good Promises/A+ compatible libraries have some equivalent to Q.all
mongoose now allows you to choose which Promise implementation.
Here I am using the node.js default system Promise (ES6) baked into nodejs
var mongoose = require('mongoose');
mongoose.Promise = global.Promise; // use system implementation
Promise.all(obj1.save(), obj2.save(), obj3.save())
.then(function(resultSaves) {
console.log('parallel promise save result :');
console.log(resultSaves);
mongoose.disconnect();
}).catch(function(err) {
console.log('ERROR on promise save :');
console.log(err);
mongoose.disconnect();
});
node --version
v4.1.1
mongoose#4.1.8
Since mongoose now supports promises you may use Promise.all().then(), so it will return when all promises are resolved.
Promise.all([
obj1.save(),
obj2.save(),
obj3.save()
])
.then(console.log)
.catch(console.error)
In fact, if you're always calling the save() method you can use the Array.map() here:
Promise.all([ obj1, obj2, obj3 ].map( obj => obj.save() )
Aaand also use es6 syntax to destructure the resulting array:
Promise.all(
[ obj1, obj2, obj3 ]
.map( obj => obj.save() )
)
.then( ([ savedObj1, savedObj2, savedObj3 ]) => {
// do something with your saved objects...
})
Try the parallel function of the async module.
var functions = [];
for (var i=0; i < docs.length; i++) {
functions.push((function(doc) {
return function(callback) {
doc.save(callback);
};
})(docs[i]));
}
async.parallel(functions, function(err, results) {
console.log(err);
console.log(results);
});
To save multiple mongoose docs in parallel, you can do something simple like this (assuming you have an array named docs of documents to save):
var count = docs.length;
docs.forEach(function(doc) {
doc.save(function(err, result) {
if (--count === 0) {
// All done; call containing function's callback
return callback();
}
});
});
A refined example on how to use async parallel would be:
async.parallel([obj1.save, obj2.save, obj3.save], callback);
Since the convention is the same in Mongoose as in async (err, callback) you don't need to wrap them in your own callbacks, just add your save calls in an array and you will get a callback when all is finished.
What about async.queue.
A simple example:
var queue = async.queue(function(obj, callback) {
return obj.save(callback);
});
for (var i in objs) {
var obj = objs[i];
// Some changes on object obj
queue.push(obj);
}
If you need a callback after the queue is emptied:
var emptyQueue = true;
var queue = async.queue(function(obj, callback) {
return obj.save(callback);
});
queue.drain = function() {
// Every callbacks are finished
// bigCallback();
};
for (var i in objs) {
var obj = objs[i];
// Some changes on object obj
queue.push(obj);
emptyQueue = false;
}
if (emptyQueue) {
// Call manually queue drain in case of the queue is empty
// and we need to call bigCallback() for example
return queue.drain();
}
#ForbesLindesay Why loading an external library when you can use mongoose implementation of promises and create your own All ?
Create a module that enhance mongoose promise with all.
var Promise = require("mongoose").Promise;
Promise.all = function(promises) {
var mainPromise = new Promise();
if (promises.lenght == 0) {
mainPromise.resolve(null, promises);
}
var pending = 0;
promises.forEach(function(p, i) {
pending++;
p.then(function(val) {
promises[i] = val;
if (--pending === 0) {
mainPromise.resolve(null, promises);
}
}, function(err) {
mainPromise.reject(err);
});
});
return mainPromise;
}
module.exports = Promise;
Then use it with mongoose:
require('./promise')
...
var tasks = [];
for (var i=0; i < docs.length; i++) {
tasks.push(docs[i].save());
}
mongoose.Promise.all(tasks)
.then(function(results) {
console.log(results);
}, function (err) {
console.log(err);
});