1

In my controller (sails.js) I have a array, that contains a list of IDs.

Those are the IDs from a another table. I need to pull each record with that ID, and send it to user.

So far I have made this code:

     suggestions.forEach(function (element, index, array){
                    Suggester.findOne({
                    "id": element.suggester_id
                    },function(err,docs){
                        suggesterResults.push(docs);
                        console.log("I am adding to array: " + docs);
                        if (index === array.length - 1) {
                            completeSend(suggesterResults);
                        }
                    });
                })
    ...
    function completeSend (results) {
        console.log("I am in complete send method"  + results)
        return res.send(results, 200);
    }

That works, but it looks like a cheat. It looks to me this is blocking code, and that is not acceptable. Is there a usual way of doing things in this situation?

Scimonster
  • 32,893
  • 9
  • 77
  • 89
Amiga500
  • 5,874
  • 10
  • 64
  • 117
  • 2
    Sounds like a job for promises. Look it up :) – Madara's Ghost Jan 27 '15 at 09:00
  • 1
    The complete code that I posted is within .done already :) – Amiga500 Jan 27 '15 at 09:03
  • 1
    *"It looks to me this is blocking code..."* That depends a lot on what `Suggester.findOne` does. If it's async (and it certainly looks like it), then the `forEach` blocks for a small fraction of a millisecond at most, I should think; then `findOne` triggers its callbacks **later** when it's finished each of those lookups. – T.J. Crowder Jan 27 '15 at 09:04
  • that `.findOne()` method runs synchronously ? – jAndy Jan 27 '15 at 09:04
  • This looks like MongoDB, if so just get all the records with `find` and the `$in` operator – adeneo Jan 27 '15 at 09:05
  • @T.J. Crowder. yes, this is part of a very large promise, actually that is a last one in chain. Would you agree that it should not block to much? – Amiga500 Jan 27 '15 at 09:07
  • @adeneo Yes, it looks like MongoDB, but it is not. I will check your suggestion as well :) – Amiga500 Jan 27 '15 at 09:07
  • Then what DB are you using ? – adeneo Jan 27 '15 at 09:08
  • This is Sails.js, and the database is PostgreSQL – Amiga500 Jan 27 '15 at 09:09
  • Seriously, I refuse to believe that's Postgre ? – adeneo Jan 27 '15 at 09:12
  • Anyway, it should be done like this -> **http://jsfiddle.net/g3qew3e6/** – adeneo Jan 27 '15 at 09:13
  • @adeneo take a look http://sailsjs.org/#/documentation/concepts/ORM :) – Amiga500 Jan 27 '15 at 09:15
  • I'm already reading it, sails uses [Waterline](https://github.com/balderdashy/waterline), which has promises and methods to query the entire set built in. It's just really inefficient to query a DB for one single record in a loop. – adeneo Jan 27 '15 at 09:19
  • The correct way to write it is something like `Suggester.find({id: suggestions.map(x => x.suggester_id)}).then(completeSend);` - sends a single query, takes one line, no dependencies. – Benjamin Gruenbaum Jan 27 '15 at 10:09

3 Answers3

3

The correct way of doing this would be

Suggester.find({
    id: suggestions.map(function(s) { return s.suggester_id; })
})
.then(completeSend);

You call find once, and pass an array of IDs, once that's done, completeSend will be called with the array of results.


You mentioned all of this is already being a part of a promise chain, in which case having a .then() there is bad practice (don't nest .then() calls!)

If that's the case, then the correct way would be:

// Some promise chain logic here
.then(function(/* suggestions? */) {
    return Suggester.find({
        id: suggestions.map(function(s) { return s.suggester_id; })
    });
})
.then(completeSend);
Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
  • This answer is the only reasonable way to solve this problem in your case in my humble opinion - both other answers suffer from [the select n+1 problem](http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue). They're both valuable as they explain important concepts but this is definitely what you should do. – Benjamin Gruenbaum Jan 27 '15 at 10:15
2

With Promises:

Promise.all(suggestions.map(function(suggestion) {
    return Suggester.findOne({"id": suggestion.suggester_id});
})
.then(completeSend);

Lets explain each line:

Promise.all(suggestions.map(function(suggestion) {
    return Suggester.findOne({"id": suggestion.suggester_id});
})

Map the suggestions array into a new array of promises. findOne returns a promise with the future result of the query.

Promise.all() is a static method that takes an array of promises, and returns a single promise that resolves when all of the promises in the array resolve successfully. That promise resolves with an array of all resolved values, in the original order, which happens to be exactly what you want.

.then(completeSend);

The promise this .then is called is the one returned from Promise.all(), so it's the equivalent of calling completeSend() with the array of all docs items, after all of the promises resolved.

Madara's Ghost
  • 172,118
  • 50
  • 264
  • 308
  • WOW...I can't wait to give this a try :). Will let you know how it go. – Amiga500 Jan 27 '15 at 09:54
  • @Wexoni note that my other answer is more correct than this. – Madara's Ghost Jan 27 '15 at 12:27
  • As I noted in the comments in the question, Sails.js does use [Waterline](https://github.com/balderdashy/waterline), which includes Bluebird and supports promises using Bluebird natively. – adeneo Jan 27 '15 at 12:41
1

If findOne completes asynchronously, and it certainly looks like it does, then that forEach will block for maybe a fraction of a millisecond. Later, findOne will call each of the callbacks, when each of its lookups completes. From a blocking perspective, that code is fine, given the proviso that findOne completes asynchronously.

But, the code has a different issue: You're assuming the callbacks will happen in order, by doing this:

if (index === array.length - 1) {
    completeSend(suggesterResults);
}

You can't make that assumption unless findOne documents it (I looked on the sails.js site; couldn't find any documentation for findOne other than an entry in a bullet list that didn't say anything). The callbacks could arrive out-of-sequence, for instance if one lookup is faster than one before it.

Instead, you'll want to track how many callbacks you've gotten, and call completeSend when you've gotten the same number as the requests you've made, rather than relying on the index.

If suggesterResults is blank when you start and nothing is going to modify suggestions while the calls are outstanding, you can use its length:

suggestions.forEach(function (element, index, array){
    Suggester.findOne({
    "id": element.suggester_id
    },function(err,docs){
        suggesterResults.push(docs);
        console.log("I am adding to array: " + docs);
        if (suggesterResults.length === array.length) {
            completeSend(suggesterResults);
        }
    });
})

But if either of those caveats isn't true, you're better off with a counter:

var waitingon = 0;
suggestions.forEach(function (element, index, array){
    ++waitingon;
    Suggester.findOne({
    "id": element.suggester_id
    },function(err,docs){
        suggesterResults.push(docs);
        console.log("I am adding to array: " + docs);
        if (--waitingon === 0) {
            completeSend(suggesterResults);
        }
    });
})

That looks like a race condition, but it isn't because this is a single-threaded environment. All of the forEach callbacks will happen before the first findOne callback does, so waitingon will rise to the appropriate level before we start decrementing it. (This also allows for the possibilty, which seems unlikely, that suggestions is sparse.)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875