MongoDB, MongooseJS and Schemas

nebula cover

I became aware of one silly reason why not to use MongoDB while listening to a lecture on databases, organized by MSCommunity. And that was a lack of schemas and what problems can that cause. Personally, to me, that seemed awesome as I like to learn about as many problems with some technology as possible before I decide whether I’ll use it or not, and this problem specifically was especially easy to solve. Looking at his demo in a presentation I had to fight the urge to interrupt and explain. Anyway, the solution is quite simple. Use MongooseJS which provides schemas and much more to ease your development.

To demonstrate how it works, let’s start with some simple project. My project structure will look like this:

/db
dbManager.js
schemas.js
index.js

Nice and clean, so let’s start from the top and work our way down. I’ve learned this way of writing code from one senior developer while programing in pair (that’s an awesome way to learn new things, by the way). Because this tutorial is about mongoose schemas, I’ll explain how to apply schematic approach to your code writing.

First, write the code you want to have, and do it by writing just function names. When you’re satisfied with the way it looks, start implementation. This is pretty good practice for naming your functions and sticking to functions main purpose. This helps beat the habit of throwing a bunch of things in functions that shouldn’t be there. For example, “I’ll just add this one more thing here…” and you end up with a messy confused code. So, name it, make it do exactly what it’s supposed to do. If it does more than that, it doesn’t belong in that specific function. That’s kind of like explaining coding practice in tutorial about something else (see how easy that can happen!).

Now I want to make simple demonstration of using schemas with MongooseJS, no need to complicate things with frontend so first i want to create database manager instance which will connect to database, setup schemas, notify me when it’s ready and finally write and read something from database.

index.js

[js]
init(){
    createDBInstance(onDBReady);
}
createDBInstance(callback){
    //new instance, pass callback or create an onReady listener
}
onDBReady(){
    writeSomeData(readSomeData);
}
writeSomeData(callback){
    //call some write function from dbManager instance and pass it the callback
}
readSomeData(data){
    //console.log data
}
[/js]

Good enough.

We are missing dbManager in the plan above so let’s do that too.

/db/dbManager.js

[js]
Constructor(optionalOnReadyCallback){
    createMongooseInstance();
    setupSchemas();
    connect(optionalOnReadyCallback);
}
[/js]

Now, for schemas we don’t really need to write any kind of plan since it will just init the schemas on passed mongoose instance. To make next step more understandable let’s first take a look how it’s used in mongoose quick start guide. Pretty simple, right?

In our case we will create mongoose instance in dbManager.js and pass reference to schemas.js to keep it clean. Let’s just jump into code, it should be self explanatory and if it isn’t, you know where the comment section is.

/db/dbManager.js

[js]
var util = require('util'),
    mongoose = require('mongoose'),
    schemas = require("./schemas.js"),
    ip = "localhost",
    dbname = "test";

var DbManager = function(){
    var self = this;
    self.createMongooseInstance();
    schemas.init(self);
    if(mongoose.connection.readyState==0)
        mongoose.connect("mongodb://"+ip+"/"+dbname);
}

var pt = DbManager.prototype;

pt.createMongooseInstance = function(){
    var self = this;

    self.db = mongoose.connection;
    self.db.on('error', function(err){
        console.log("mongoose conection error: " + err);
    });
    self.db.once('open', function callback () {
        console.log("connected to mongo, nice...");
    });
    self.db.on('connected', function(){
        console.log("mongo db: connected!");
    });
}

module.exports = DbManager;
[/js]

/db/schemas.js

[js]
var mongoose = require("mongoose");

module.exports.init = function(parent){
    var self = parent,
        ObjectId = mongoose.Schema.Types.ObjectId;

    self.schemas = {};

    self.schemas.Users = mongoose.Schema({
        name: String,
        email: String
    });

    var schemaList = ["Users"];

    for(var s=0;s<schemaList.length;s++){
        if(mongoose.modelNames().indexOf(schemaList[s])<0){
            self[schemaList[s]] = mongoose.model(schemaList[s], self.schemas[schemaList[s]]);
        } else {
            self[schemaList[s]] = mongoose.model(schemaList[s]);
        }
    }
}
[js]

now in index.js, for a test run we can just create dbManager instance to check if everything works as expected
[js]
var DbManager = require("./db/dbManager.js")

var db = new DbManager();
[/js]


$ node index.js
mongo db: connected!
connected to mongo, nice...

Alright, everything works. Now we need to cover writing/reading and later I’ll add one more schema and explain how to link and populate result from 2 schemas, but before we do that we need to stick to the original idea we had at the beginning of the tutorial and write all those functions. Just as soon as I got the craving to quickly finish this and mess the code up comes the disciplinary slap of from the top of the article 🙂

index.js

[js]
var util = require("util"),
    DbManager = require("./db/dbManager.js"),
    db = null;

init();

function init(){
    createDBInstance(onDBReady);
}
function createDBInstance(callback){
    db = new DbManager(callback);
}
function onDBReady(){
    writeSomeData(readSomeData);
}
function writeSomeData(callback){
    if(db!=null){
        db.addNewUser({
            name: "Igor Neuhold",
            email: "igor@fenixapps.com"
        }, callback);
    }
}
function readSomeData(){//removed argument
    if(db!=null){
        db.getUsers(function(users){
            console.log(util.inspect(users));
        });
    }
}
[/js]

/db/dbManager.js

[js]
var util = require('util'),
    mongoose = require('mongoose'),
    schemas = require("./schemas.js"),
    ip = "localhost",
    dbname = "test";

var DbManager = function(onConnectCallback){//optional callback
    var self = this;
    self.createMongooseInstance(onConnectCallback);//passed it here
    schemas.init(self);
    if(mongoose.connection.readyState==0)
        mongoose.connect("mongodb://"+ip+"/"+dbname);
}

var pt = DbManager.prototype;

pt.createMongooseInstance = function(onConnectCallback){
    var self = this;

    self.db = mongoose.connection;
    self.db.on('error', function(err){
        console.log("mongoose conection error: " + err);
    });
    self.db.once('open', function callback () {
        console.log("connected to mongo, nice...");
        //check if callback is defined
        if(onConnectCallback!=null){//probably smarter to check if function
            onConnectCallback();
        }
    });
    self.db.on('connected', function(){
        console.log("mongo db: connected!");
    });
}

//added 2 new functions
pt.addNewUser = function(data, callback){
    var self = this;

    var newUser = new self.Users(data);
    newUser.save(function(err, res){
        if(err)return console.error(err);
        else callback();
    });
}

pt.getUsers = function(callback){
    var self = this;

    self.Users.find({}, function(err, docs){
        if(err)return console.error(err);
        else {
            callback(docs);
        };
    });
}

module.exports = DbManager;
[/js]

If you run it now, you should see array with our db user object(s). And, now, for the final lesson, let’s add new schema and link it somehow to our user object. Take a look at schemas.js.

[js]
var mongoose = require("mongoose");

module.exports.init = function(parent){
    var self = parent,
        ObjectId = mongoose.Schema.Types.ObjectId;

    self.schemas = {};

    self.schemas.Users = mongoose.Schema({
        name: String,
        email: String
    });

    self.schemas.GamesPlayed = mongoose.Schema({
        playerId: {type: ObjectId, ref: "Users"},
        score: Number
    });

    var schemaList = ["Users", "GamesPlayed"];

    for(var s=0;s<schemaList.length;s++){
        if(mongoose.modelNames().indexOf(schemaList[s])<0){
            self[schemaList[s]] = mongoose.model(schemaList[s], self.schemas[schemaList[s]]);
        } else {
            self[schemaList[s]] = mongoose.model(schemaList[s]);
        }
    }
}
[/js]

If you want to get data with populate method, you’ll need to define type in schema with an object containing just a little bit more info than how you would usually define it.
{type: ObjectId, ref: "Users"}
Type of ObjectId is a must if you want to point it to other schema and ref is the reference, a name of that schema you want to point to.
So, with all those changes now you can take a look at changes i’ve made to make use if this. Also, I’ve copied all the code for you guys who don’t read much but are mostly just copy/pasting everything. If you’re interested only in important stuff, take a look at last function in dbManager.js containing populate function.

index.js

[js]
var util = require("util"),
    DbManager = require("./db/dbManager.js"),
    db = null;

init();

function init(){
    createDBInstance(onDBReady);
}
function createDBInstance(callback){
    db = new DbManager(callback);
}
function onDBReady(){
    writeSomeData(readSomeData);
}
function writeSomeData(callback){
    if(db!=null){
        db.addNewUser({
            name: "Igor Neuhold",
            email: "igor@fenixapps.com"
        }, callback);
    }
}
function readSomeData(){//removed argument
    if(db!=null){
        db.getUsers(function(users){
            console.log(util.inspect(users));
            insertGameWithUser(users[0]);
        });
    }
}

function insertGameWithUser(user){
    if(db!=null){
        db.insertPlayedGame(user, function(){
            db.getPlayedGames(showGames);
        });
    }
}

function showGames(games){
    console.log(util.inspect(games));
}
[/js]

db/dbManager.js

[js]
var util = require('util'),
    mongoose = require('mongoose'),
    schemas = require("./schemas.js"),
    ip = "localhost",
    dbname = "test";

var DbManager = function(onConnectCallback){//optional callback
    var self = this;
    self.createMongooseInstance(onConnectCallback);//passed it here
    schemas.init(self);
    if(mongoose.connection.readyState==0)
        mongoose.connect("mongodb://"+ip+"/"+dbname);
}

var pt = DbManager.prototype;

pt.createMongooseInstance = function(onConnectCallback){
    var self = this;

    self.db = mongoose.connection;
    self.db.on('error', function(err){
        console.log("mongoose conection error: " + err);
    });
    self.db.once('open', function callback () {
        console.log("connected to mongo, nice...");
        //check if callback is defined
        if(onConnectCallback!=null){//probably smarter to check if function
            onConnectCallback();
        }
    });
    self.db.on('connected', function(){
        console.log("mongo db: connected!");
    });
}

//added 2 new functions
pt.addNewUser = function(data, callback){
    var self = this;

    var newUser = new self.Users(data);
    newUser.save(function(err, res){
        if(err)return console.error(err);
        else callback();
    });
}

pt.getUsers = function(callback){
    var self = this;

    self.Users.find({}, function(err, docs){
        if(err)return console.error(err);
        else {
            callback(docs);
        };
    });
}

pt.insertPlayedGame = function(data, callback){
    var self = this;

    var game = {
        playerId: data._id,
        score: Math.round(Math.random()*1000)
    };
    var newGame = new self.GamesPlayed(game);
    newGame.save(function(err, res){
        if(err)return console.error(err);
        else callback();
    });
}

pt.getPlayedGames = function(callback){
    var self = this;

    var q = self.GamesPlayed.find({}).populate("playerId");
    q.exec(function(err, docs){
        if(err)return console.error(err);
        else {
            callback(docs);
        };
    });
}

module.exports = DbManager;
[/js]

The final result should look something like this if you cleaned your collections before running the example. If not, then you’ll have a bunch of duplicates, but who cares, this is just an example.

$ node index.js
mongo db: connected!
connected to mongo, nice...
[ { _id: 55abd2a74e8f19561736c736,
name: 'Igor Neuhold',
email: 'igor@fenixapps.com',
__v: 0 } ]
[ { _id: 55abd2a74e8f19561736c737,
playerId:
{ _id: 55abd2a74e8f19561736c736,
name: 'Igor Neuhold',
email: 'igor@fenixapps.com',
__v: 0 },
score: 278,
__v: 0 } ]

That’s all folks.
Thanks for reading.