10

As MongoDB database access and initialization is asynchronous on Node.js, I would like to define one module per collection that exports wrapped db calls after db initialization.

Such a "Cars.model.js" module looks like that:

var db = require("mongodb");
db.collection("cars", function(err, col) {
    exports.getCars = function(callback) {
        col.find({}, callback);
    };
});

so that other modules can run:

var carModel = require("Cars.model.js").getCars;
getCars(err, cars) {
    // (do something with cars here...)
};

It happened to me that getCars was undefined, because db access was not yet initialized at the time my second module was run.

How do you deal with creating such asynchronous db models?

Adrien Joly
  • 5,056
  • 4
  • 28
  • 43

3 Answers3

11

You cannot write to exports after you've left the file. You must be blocking. To avoid being blocking I would use lazy loading of resources.

var carCol;
var carEmitter = new require("events").EventEmitter;


exports.getCars = function(callback) {
  // if no car collection then bind to event
  if (carCol === undefined) {
    carEmitter.on("cars-ready", function() {
      callback(carCol);
    });
  } else {
    // we have cars, send them back
    callback(carCol);
  }
}

db.collection("cars", function(err, col) {
  // store cars
  carCol = col;
  // tell waiters that we have cars.
  carEmitter.emit("cars-ready");
});

Use event emitters to emulate lazy loading. You may want to generalize to a LazyLoadedCollection class/object to make the code neater / more DRY.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • 2
    @adrienjoly not very DRY or extendable though! – Raynos Jun 21 '11 at 15:07
  • I believe that most of the asynchronous complexity could be moved to the application init module, by populating a collection cache before any logic (and thus, models) is run. I will propose some code as soon as stackoverflow allows me to answer my own question (in 5 hours now) ;-) – Adrien Joly Jun 21 '11 at 15:14
  • 1
    @adrienjoly I would recommend againts blocking in your init until it's loaded. Best to let it load in the background and add requests for the data to a buffer whilst it's loading. – Raynos Jun 21 '11 at 15:44
  • 1
    i did that first, but my application modules could not compile until the collection callbacks were triggered by the asynchronous db loader, because of the models' exports defined in the callback... – Adrien Joly Jun 22 '11 at 08:33
  • You CAN write to module.exports asynchronously actually – tu4n Jan 07 '17 at 13:47
2

I'm a newbe so don't be mad at me...

I'm using promises to do so:

var db = require("mongodb"),
Q = require('q'),
getCarsDefer = Q.defer();

exports.getCars = getCarsDefer.promise;

db.collection("cars", function(err, col) {
  if(err){
    getCarsDefer.reject(err);
  }else{
     getCarsDefer.resolve(Q.ninvoke(col,'find'));
  };
});

So you can get your cars like that:

var carModel = require("Cars.model.js").getCars;
getCars.then(cars) {
    // (do something with cars here...)
};

It this is a bad idea, please let me know because it's what i'm doing right now.

pykiss
  • 949
  • 12
  • 15
0

I believe this simple solution works: replace the asynchronous call to getCars() by a synchronous call to a collection cache that would be populated before models can be called.

The main.js application starter module:

var db = require("mongodb");
exports.collectionCache = {};
db.collection("cars", function(err, col) {
    exports.collectionCache["cars"] = col;
    // ready => start application logic (incl. models) here
});  

Thus "Cars.model.js" would look like that:

var col = require("main").collectionCache; // populated
exports.getCars = function(callback) {
    col["cars"].find({}, callback);
};

This solution moves the asynchronous problem from models to the root database model, making models simpler to develop.

Adrien Joly
  • 5,056
  • 4
  • 28
  • 43
  • 1
    This also increases server start up time because you have to wait for all the collections to be loaded. I also have my doubts this works because your writing to `exports` asynchronously and I don't know whether exports is copied by reference or by value. – Raynos Jun 22 '11 at 08:40
  • Indeed, it does increase server startup time, but simplifies the development of models, and optimizes the number of simultaneous opened collections. – Adrien Joly Jun 24 '11 at 12:50
  • Concerning the asynchronous population of `exports.collectionCache`, I believe that it is correct, as javascript objects are always kept as references. It would not work if I was redefining this export asynchronously, though. – Adrien Joly Jun 24 '11 at 12:53
  • 1
    I still think your logic is backwards. Models.js should not require `main`. You just need to think asynchronous not synchronous. Your solving the problem with a hack. – Raynos Jun 24 '11 at 12:56
  • 1
    the most important things for me are that the code works, is readable and easy to maintain. i don't mind hacking! ^^ – Adrien Joly Jun 25 '11 at 18:10