1

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?

1
  • why you don't create the files after the user and add the user Id in the definition of file? Commented Oct 4, 2016 at 17:32

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.