Node.js detect when two mongoose find are finished

2019-04-28 12:55发布

问题:

I'm trying to initialize two input with autocomplete with this library. When I load my page, I will trigger an Ajax to initialize two input text.

But I don't know how I can detect when all my mongoose find are completed.

Here is my server side code :

app.post('/init/autocomplete', function(req, res){
    var autocomplete = {
        companies: [],
        offices: []
    };

    // Find all companies
    Company.find({}, function(err, companies) {
        if (err) throw err;

        companies.forEach(function(company) {
            autocomplete.companies.push({value: company.name})
        });

        console.log('One');
    });

    // Find all offices
    Office.find({}, function(err, offices) {
        if (err) throw err;

        offices.forEach(function(office) {
            autocomplete.offices.push({value: office.name})
        });

        console.log('Two');
    });

    console.log('Three');

    // res.json(autocomplete);
});

I know than the find method is async. That is why I see my console.log() in this order :

Three
One
Two

How can I do to trigger console.log('Three'); when the Company.find and Office.find are finished ?

I want to see the console.log('Three'); at the last position.

Edit :

I think I can do this way :

app.post('/init/autocomplete', function(req, res){
    var autocomplete = {
        companies: [],
        offices: []
    };

    // Find all companies
    Company.find({}, function(err, companies) {
        if (err) throw err;

        companies.forEach(function(company) {
            autocomplete.companies.push({value: company.name})
        });

        // Find all offices
        Office.find({}, function(err, offices) {
            if (err) throw err;

            offices.forEach(function(office) {
                autocomplete.offices.push({value: office.name})
            });

            res.json(autocomplete);
        });
    });
});

But I don't know if it's the good way. Maybe using promise will be better ? I'm open for all suggestions.

回答1:

Mongoose has built-in support for promises which provide a clean way to wait for the completion of multiple async query operations with Promise.all:

// Tell Mongoose to use the native Node.js promise library.
mongoose.Promise = global.Promise;

app.post('/init/autocomplete', function(req, res){
    var autocomplete = {
        companies: [],
        offices: []
    };

    // Call .exec() on each query without a callback to return its promise.
    Promise.all([Company.find({}).exec(), Office.find({}).exec()])
        .then(results => {
            // results is an array of the results of each promise, in order.
            autocomplete.companies = results[0].map(c => ({value: c.name}));
            autocomplete.offices = results[1].map(o => ({value: o.name}));
            res.json(autocomplete);
        })
        .catch(err => {
            throw err; // res.sendStatus(500) might be better here.
        });
});


回答2:

use Promise. There are ways to control parallel and series. and your code tends to be much readable. My method of dealing with parallel is to execute the async part first, then when the result have been collected, do the synchronous part.

app.post('/init/autocomplete', function(req, res){
    // Find all companies
    // the query action below is not executed, just return PromiseObject for now
    var findCompanies = new Promise((resolve, reject) => {
      Company.find({}, function(err, companies) {
          if (err) reject(err);
          resolve(companies)                   
       });
    })

    // Find all offices
    // the query action below is not executed, just return PromiseObject for now
    var findOffices = new Promise((resolve, reject) => {
      Office.find({}, function(err, offices) {
          if (err) reject(err);

          resolve(offices)
      });
    })

    // this is the execution part, in OOP world, you can think this is main()
    // execute and wait until each action(Promise Object) is complete before finally returning an array.
    return Promise.all([findCompanies, findOffices])
                  .then(array => {
                       console.log(array) // [ [companies] , [offices]  ]
                       //the difficult async part is done, with just few lines

                       console.log(array[0]) // [companies] array of companies
                       console.log(array[1]) // [offices] array of offices

                       // now you can safely manipulate using forEach.
                       // basically, this part is for the synchronous action               

                        var autocomplete = {};

                        array[0].forEach(function(company) {
                            autocomplete.companies.push({value: company.name})
                        });
                        array[1].forEach(function(office) {
                            autocomplete.office.push({value: office.name})
                        });
                       res.json(autocomplete)
                  })
});

the code above, findCompanies and FindOffices are Promise Object store in variable(it is not yet executed). Next, with Promise.all(), we run all the Promise Objects, and it will wait until each action is complete before finally returning an array.

the returned array contains another two array. The sequence of this array is the same as the sequence of actions you pass to Promise.all()

if you findOffices first, then findCompanies. It will return [[offices],[companies]] Promise.all([findOffices, findCompanies]) will return [[offices], [companies]]

vice versa

Promise.all([findCompanies, findOffices]) will return [[companies], [offices]]



回答3:

With Promises you'll have a more clear code to maintain with not so much overhead, using Mongoose you have two options. Using the native Mongoose promises which is fine for basic use cases. But Mongoose promises are deprecated at this time as a warning will show up.

So to switch to native Promises:

// Native JS Promise
mongoose.Promise = global.Promise;

Or to the more advanced Bluebird Promise library:

// Bluebird
mongoose.Promise = require('bluebird');

An MVCE with native JS promises and Bluebird commented out.

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const assert = require('assert');

mongoose.connect("mongodb://localhost:33023/test_1");

// Native JS Promise
mongoose.Promise = global.Promise;

// Bluebird
// mongoose.Promise = require('bluebird');


// Schema creation
var Company = mongoose.model('Company', new Schema({
  name: String
}));

var Office = mongoose.model('Office', new Schema({
  name: String
}));


var acme = new Company({
  name: 'ACME'
});

var HR = new Office({
  name: 'Human Resources'
});

acme
  .save()
  .then(HR.save());

var query = Company.find({});
assert.equal(query.exec().constructor, global.Promise);
// assert.equal(query.exec().constructor, require('bluebird'));

query.exec()
.then(function(results) {
  console.log(results);
})
.then(Office.find({})
.then(function(results) {
  console.log(results);
}));

Documentation for Mongoose Promises.



回答4:

The method you specified can be used but for a large number of async operation, it will result in callback from hell. In order to avoid this it is always better to write code in a sequential and orderly manner. In your case you can use a library like async to achieve this, since mongoose doesn't return a Promise Object. You can refer to this link for more information on async library.

async.series([
    function(callback) {
        // do some stuff ...
        console.log('one');
        callback(null, 'one');
    },
    function(callback) {
        // do some more stuff ...
        console.log('two');
        callback(null, 'two');
    }
],
// optional callback
function(err, results) {
    // results is now equal to ['one', 'two']
    console.log('three');
});

The third log will only be written once tasks one and two are completed. series function runs tasks one by one. You can use the eachSeries to run all tasks parallely.



回答5:

I'm quite new to mongoose but I just had a similar problem and I solved it by using a cursor()

let cursor = Company.find().cursor();
    cursor.on('data', function(doc){
        //do something with doc
    })


    cursor.on('close', function() {
        console.log("done");
    })