MEAN web development #7: MongoDB and Mongoose

Last week we’ve seen some of the basic functionality of AngularJS, at least enough to get you started. Before that we’ve seen Node.js and Express. So that’s EAN and we’re left with the M. Well, Dial M for MongoDB because that’s what we’re going to look at this week.

  1. MEAN web development #1: MEAN, the what and why
  2. MEAN web development #2: Node.js in the back
  3. MEAN web development #3: More Node.js
  4. MEAN web development #4: All aboard the Node.js Express!
  5. MEAN web development #5: Jade and Express
  6. MEAN web development #6: AngularJS in the front
  7. MEAN web development #7: MongoDB and Mongoose
  8. MEAN web development #8: Sockets will rock your socks!
  9. MEAN web development #9: Some last remarks

As usual you can find the examples for this post on my GitHub page in the mean7-blog repository.

Hello persistent data

I’ve already written an entire post on NoSQL and MongoDB, A first look at NoSQL and MongoDB in particular. I’ve already told you to read it in the first part of this series, MEAN web development #1: MEAN, the what and why. If you haven’t read either of those I suggest you do so before continuing because I won’t repeat how to install MongoDB and MongoVUE. Don’t worry I’ll wait…

Before we continue I should mention that everything we’re going to do is async. That means lots of callback functions. We don’t want to block our main thread after all! It also means that the callbacks may not be called in the same order as their ‘parent functions’ are. Or that the records are actually inserted before we query them! Because I wanted to keep it simple in the complete example file I haven’t nested all examples in callbacks, but keep in mind that you may get some odd results. It worked fine for me by the way, if you get some weird results try running the examples one by one (simply commenting out the others).

So let’s just get a Node.js server up and running and write some data to the database real quick! You’ll be surprised how easy it is. First of all install the MongoDB driver using npm (npm install mongodb).
Next we’ll make a connection to our MongoDB instance.

var app = require('express')();
var MongoClient = require('mongodb').MongoClient;
     
var urlWithCreds = 'mongodb://user:password@localhost:27017/local';
var url = 'mongodb://localhost:27017/local';
MongoClient.connect(url, function (err, db) {
    if (err) {
        console.log(err);
    } else {
        console.log('Connected to the database.');
        db.close();
    }
});
var server = app.listen(80, '127.0.0.1');

So first of all we require Express (which isn’t necessary for MongoDB) and MongoDB. We take the MongoClient property of the MongoDB module. We use this client to connect to the database using the connect function, which takes a URI and a callback function. The callback has a MongoError (in case you can’t log in, for example if you have wrong credentials) and a Db as parameters.
We can use the Db object to do all kinds of stuff like creating and dropping databases, collections and indices and do our CRUD operations (Create, Read, Update, Delete). Let’s insert a simple object.

var url = 'mongodb://localhost:27017/local';
MongoClient.connect(url, function (err, db) {
    if (err) {
        console.log(err);
    } else {
        var artist = {
            name: 'Massive Attack',
            countryCode: 'GB'
        };
        var collection = db.collection('artists');
        collection.insertOne(artist);
        console.log(artist._id);
        db.close();
    }
});

As you can see we use the db parameter to get a collection (the MongoDB variant of a database table) using the collection function. If the collection does not exist it will create one automatically. We can then simply insert an object using the insertOne function of the collection. Now something funny has happened. After calling insertOne our artist object suddenly has an _id property. MongoDB uses this _id to uniquely identify objects.
So that wasn’t so bad right? Let’s look at other CRUD functionality!

CRUD with MongoDB

So let’s retrieve the record we just inserted. We can do this using the findOne function of the collection.

MongoClient.connect(url, function (err, db) {
    if (err) {
        console.log(err);
    } else {
        var collection = db.collection('artists');
        collection.findOne({ name: 'Massive Attack' }, function (err, artist) {
            if (err) {
                console.log(err);
            } else {
                console.log(artist);
            }
            db.close();
        });
    }
});

So the object that is passed to the findOne function is actually a search parameter. In this case we’re looking for documents (or records) that have a name equal to ‘Massive Attack’. The second parameter is a callback function that gives us an error, if any occurred, and the document that was retrieved. If you ran the previous example multiple times Massive Attack will be in your database more than once (having different values for _id), in this case findOne simply returns the first document it finds.

So let’s insert a few more artists, just so we’ve got a little set to work with. We can use the insertMany function for this.

collection.insertMany([
{
    name: 'The Beatles',
    countryCode: 'GB',
    members: [
        'John Lennon',
        'Paul McCartney',
        'George Harrison',
        'Ringo Starr'
    ]
},
{
    name: 'Justin Bieber',
    countryCode: 'No one wants him'
},
{
    name: 'Metallica',
    countryCode: 'USA'
},
{
    name: 'Lady Gaga',
    countryCode: 'USA'
}
], function (err, result) {
    if (err) {
        console.log(err);
    } else {
        console.log(result);
    }
});

Now you may think there’s a findMany function as well, but it’s actually just called find. find returns a Cursor which is something like an array, but not quite. We can use the toArray method though. The find function has a query parameter which is just an object that describes what fields of a document must have which values. We can search fields with AND, OR, NOT, IN, greater than, lesser than, regular expressions and everything you’re used to in SQL databases.

var findCallback = function (err, artists) {
    if (err) {
        console.log(err);
    } else {
        console.log('\n\nFound artists:');
        artists.forEach(function (a) {
            console.log(a);
        });
    }
};

// All documents.
collection.find().toArray(findCallback);

// Name not equal to Justin Bieber.
collection.find({ name: { $ne: 'Justin Bieber' } }).toArray(findCallback);

// Name equal to Massive Attach or name equal to The Beatles.
collection.find({ $or: [{ name: 'Massive Attack' }, { name: 'The Beatles' }] }).toArray(findCallback);

// Members contains John Lennon.
collection.find({ members: 'John Lennon' }).toArray(findCallback);

Now let’s update a record.

collection.findOneAndUpdate({ name: 'Massive Attack' },
    { $set: {
        cds: [
            {
                title: 'Collected',
                year: 2006,
                label: {
                    name: 'Virgin'
                },
                comment: 'Best Of'
            },
            {
                title: 'Mezzanine',
                year: 1998,
                label: 'Virgin'
            },
            {
                title: 'No Protection: Massive Attack v Mad Professor',
                year: 1995,
                label: 'Circa Records',
                comment: 'Remixes'
            },
            {
                title: 'Protection',
                year: 1994,
                label: {
                    name: 'Circa'
                }
            }
        ]
        }
    }, function (err, result) {
    console.log('\n\nUpdated artist:');
    if (err) {
        console.log(err);
    } else {
        console.log(result);
    }
});

Here we see the findOneAndUpdate in action. Alternatively we could’ve used updateOne. And for multiple updates we can use updateMany.

Now let’s delete a record. There’s one guy I really don’t want in my database (yes, I’ve added him so I wouldn’t feel guilty about deleting him).  And for this we can, of course, use findOneAndDelete.

collection.findOneAndDelete({ name: 'Justin Bieber' }, function (err, result) {
    console.log('\n\nDeleted artist:');
    if (err) {
        console.log(err);
    } else {
        console.log(result);
    }
});

Really no surprises there. Alternatively there’s deleteOne and to delete many use, you guessed it, deleteMany.

Mongoose

So MongoDB with Node.js looks really good, right? It wasn’t very hard to use. It’s really just a matter of working with JavaScript objects. And as we all know JavaScript objects are very dynamic. In the previous examples we’ve already seen that some artists have a member property defined and then when we updated we all of a sudden had a cds property and some CD’s have a comment while others don’t… And MongoDB has no problem with it at all. We just save and fetch what is there.

Now try this.

var app = require('express')();
var MongoClient = require('mongodb').MongoClient;

var Artist = function (name, activeFrom, activeTo) {
    if (!(this instanceof Artist)) {
       return new Artist(name, activeFrom, activeTo);
    }
    var self = this;
    self.name = name;
    self.activeFrom = activeFrom;
    self.activeTo = activeTo;
    self.yearsActive = function () {
        if (self.activeTo) {
            return self.activeTo - self.activeFrom;
        } else {
            return new Date().getFullYear() - self.activeFrom;
        }
    };
};

var url = 'mongodb://localhost:27017/local';
MongoClient.connect(url, function (err, db) {
    if (err) {
        console.log(err);
    } else {
        var collection = db.collection('artists');
        // Empty the collection
        // so the next examples can be run more than once.
        collection.deleteMany();
        
        var massiveAttack = new Artist('Massive Attack', 1988);
        console.log('\n\n' + massiveAttack.name + ' has been active for ' + massiveAttack.yearsActive() +  ' years.');
        
        collection.insertOne(massiveAttack);
        
        collection.findOne({ name: massiveAttack.name }, function (err, result) {
            if (err) {
                console.log(err);
            } else {
                try {
                    console.log('\n\n' + result.name + ' has been active for ' + result.yearsActive() +  ' years.');
                } catch (ex) {
                    console.log(ex);
                }
            }
        });
        
    }
});

var server = app.listen(80, '127.0.0.1');

What happens is that MongoDB doesn’t store the yearsActive function nor the constructor function. What MongoDB stores are just the non-function values. The result is that when we retrieve our object it will no longer be an Artist object, but just an object that just so happens to have the same properties as an Artist.

This is where Mongoose comes to the rescue! Mongoose adds a schema to your MongoDB objects. Let’s see how that works.

To add Mongoose to your project you can install it using npm install mongoose.

So first we can use mongoose.connect to get a connection to the database.

var app = require('express')();
var mongoose = require('mongoose');

var url = 'mongodb://localhost:27017/local';
mongoose.connect(url);
var db = mongoose.connection;
db.on('error', function (err) {
    console.log(err);
});
db.once('open', function (callback) {
    // ...
});

var server = app.listen(80, '127.0.0.1');

After that we can define a schema using the Schema function.

db.once('open', function (callback) {
    var artistSchema = mongoose.Schema({
        name: String,
        activeFrom: Number,
        activeTo: Number
    });
    artistSchema.methods.yearsActive = function () {
        var self = this;
        if (self.activeTo) {
            return self.activeTo - self.activeFrom;
        } else {
            return new Date().getFullYear() - self.activeFrom;
        }
    };
});

And as you can see I’ve appended the yearsActive function to the artistSchema.methods object. After that we can create a Model using mongoose.model.

var Artist = mongoose.model('Artist', artistSchema);

And after that the Artist variable (a Model) is actually the portal to your collection. It’s also a constructor function for artists. So let’s create our Massive Attack artist.

var massiveAttack = new Artist({ name: 'Massive Attack', activeFrom: 1988 });
console.log('\n\n' + massiveAttack.name + ' has been active for ' + massiveAttack.yearsActive() + ' years.');

And then we can save it using the save function.

massiveAttack.save(function (err, result) {
    if (err) {
        console.log(err);
    } else {
        // ...
    }
});

And now that the artist is saved let’s retrieve it and call that yearsActive function again. We can simply retrieve our object using Model.findOne.

Artist.findOne({ name: massiveAttack.name }, function (err, result) {
    if (err) {
        console.log(err);
    } else {
        try {
            console.log('\n\n' + result.name + ' has been active for ' + result.yearsActive() +  ' years.');
        } catch (ex) {
            console.log(ex);
        }
    }
});

And here I’ve put the findOne directly in the callback function of save, which I didn’t do before. I needed this because calling findOne directly after save didn’t yield any results (timing issue I guess). More importantly it did successfully execute the yearsActive function!

And like with the regular MongoDB driver we can use find, remove, findOneAndRemove and findOneAndUpdate.

So we’ve looked at the MongoDB driver and at the problem of schemaless objects which Mongoose fixes. I can recommend practicing a bit, as it’s really very easy to drop, create, insert, update, read and remove data, and reading the API documentation of both. We’ve only scratched the surface here, but it got you on your way.

And of course I’m going to recommend some additional reading. The Node.js Succinctly book is just a great resource for Node.js in general and it has a tiny bit on MongoDB and SQLite as well. I can also recommend MongoDB Succinctly. And Getting MEAN from Manning even has a chapter on MongoDB and Mongoose.

Happy coding!