Reputation: 842
I have User and File models in Sequlize. User can have several files. I have association db.User.hasMany(db.File, {as: 'Files', foreignKey: 'userId', constraints: false});
I want to init User object with several Files and save it to database.
I wrote next code:
var files = [];
var file1 = models.File.build();
file1.name = "JPEG";
file1.save().then(function () {
});
files.push(file1);
var file2 = models.File.build();
file2.name = "PNG";
file2.save().then(function () {
});
files.push(file2);
var newUser = models.User.build();
newUser.email = email;
newUser.save().then(function (usr) {
files.forEach(function (item) {
newUser.addFile(item);
});
});
But I found a bug, sometimes several files were not associated with the user.
I found(in nodejs logs) commands(update) for setting foreign keys for these files. But commands were not executed. Foreign keys(userId) of several files were empty.
I think the problem is in asynchronous queries. How to organize code structure to avoid this bug?
Upvotes: 1
Views: 1777
Reputation: 759
The problem is exactly what you think it is, the async code.
You need to move the functions inside of the callbacks otherwise the code gets run before the file is created.
JavaScript doesn't wait before moving on to the next line so it'll run the next line whether its done or not. It has no sense of waiting before moving.
So you're basically adding something that doesn't yet exist because it doesn't wait for the file to be saved before moving on.
This would work though, just moving the code inside of the then
callbacks:
var files = [];
var file1 = models.File.build();
file1.name = "JPEG";
file1.save().then(function () {
files.push(file1);
var file2 = models.File.build();
file2.name = "PNG";
file2.save().then(function () {
files.push(file2);
var newUser = models.User.build();
newUser.email = email;
newUser.save().then(function (usr) {
files.forEach(function (item) {
newUser.addFile(item);
});
});
});
});
But that's messy. Instead you can chain promises like this:
var files = [];
var file1 = models.File.build();
file1.name = "JPEG";
file1.save()
.then(function(file1) {
files.push(file1);
var file2 = models.File.build();
file2.name = "PNG";
return file2.save();
})
.then(function(file2) {
files.push(file2);
var newUser = models.User.build();
newUser.email = email;
return newUser.save();
})
.then(function(newUser) {
files.forEach(function(item) {
newUser.addFile(item);
});
});
Now that's a bit cleaner but still a bit messy and also a bit confusing. So you can use generator functions instead like this:
var co = require('co');
co(function*() {
var files = [];
var file1 = models.File.build();
file1.name = "JPEG";
yield file1.save();
files.push(file1);
var file2 = models.File.build();
file2.name = "PNG";
yield file2.save();
files.push(file2);
var newUser = models.User.build();
newUser.email = email;
newUser.save();
files.forEach(function(item) {
newUser.addFile(item);
});
});
Now that's much better.
Look closely and you see what's happening. co
accepts a generator function which is basically a regular function with asterisks *
. This is a special functions which adds yield
expression support.
yield
expressions basically wait for the then()
callback to be called before moving on and if the then
callback has a argument then it'll return it as well.
So you can do something like:
var gif = yield models.File.create({
name: 'gif'
});
instead of:
models.File.create({
name: 'gif'
}).then(function(gif) {
});
You have to use a small node module called co
for this though just npm install --save co
Upvotes: 2