While working directly with the mongodb
module is great, it's also a bit raw and lacks any sense of developer friendliness that we've come to expect working with frameworks such as Express in Node.js. Mongoose is a great third-party framework that makes working with MongoDB a breeze. Mongoose is an elegant mongodb
object modeling for Node.js.
What that basically means is that Mongoose gives us the power to organize our database by using schemas (also known as model definitions) and providing powerful features to our models such as validation, virtual properties, and more. Mongoose is a great tool as it makes working with collections and documents in MongoDB feel much more elegant. The original mongodb
module is a dependency of Mongoose, so you can think of Mongoose as being a wrapper on top of mongodb
much like Express is a wrapper on top of Node.js—both abstract away a lot of the "raw" feeling and give you easier tools to work with directly.
It's important to note that Mongoose is still MongoDB, so everything you're familiar with and used to will work pretty much the same way; only the syntax will change slightly. This means that the queries and inserts and updates that we know and love from MongoDB work perfectly fine with Mongoose.
Let's take a look at some of the features that Mongoose has to offer and what we'll take advantage of to make our lives easier when developing apps that heavily rely on a MongoDB database.
In Mongoose, schemas are what we use to define our models. Think of them as a blueprint that all models you create throughout the app will derive from. Using schemas, you can define much more than the simple blueprint of a MongoDB model. You can also take advantage of the built-in validation that Mongoose provides by default, add static methods, virtual properties, and more!
The first thing we do while defining a schema for a model is build a list of every field we think we will need for a particular document. The fields are defined by type, and the standard datatypes you would expect are available as well as a few others:
Here is an example of a basic Mongoose Schema definition:
var mongoose = require('mongoose'), Schema = mongoose.Schema; var Account = new Schema({ username: { type: String }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false } });
Here we define our schema for an Accounts
collection. The first thing we do is require mongoose
and then define a schema object using mongoose.Schema
in our module. We define a schema by creating a new Schema
instance with a constructor object that defines the schema. Each field in the definition is a basic JavaScript object with type
, and then an optional default
value.
A model in Mongoose is a class that can be instantiated (defined by a schema). Using schemas, we define models and then use them like a regular JavaScript object. The benefit is that the model object has the added bonus of being backed by Mongoose, so it also includes features such as saving, finding, creating, and removing. Let's take a look at defining a model using a schema and then instantiating a model and working with it.
The first thing we need to do is install Mongoose so that it's available to use within our mongotest
project:
$ npm install mongoose
Continuing with editing our experimentation file, mongotest/test.js
, include the following block of code after the existing code:
var mongoose = require('mongoose'), Schema = mongoose.Schema; mongoose.connect('mongodb://localhost:27017/mongotest'), mongoose.connection.on('open', function() { console.log('Mongoose connected.'), }); var Account = new Schema({ username: { type: String }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false } }); var AccountModel = mongoose.model('Account', Account); var newUser = new AccountModel({ username: 'randomUser' }); console.log(newUser.username); console.log(newUser.date_created); console.log(newUser.visits); console.log(newUser.active);
Running the preceding code should result in something similar to the following:
$ node test.js randomUser Mon Jun 02 2014 13:23:28 GMT-0400 (EDT) 0 false
Creating a new
model is great when you're working with new documents and you want a way to create a new instance, populate its values, and then save it to the database:
var AccountModel = mongoose.model('Account', Account);
var newUser = new AccountModel({ username: 'randomUser' });
newUser.save();
Calling .save
on a Mongoose model will trigger a command to MongoDB that will perform the necessary insert
or update
statements to update the server. When you switch over to your mongo shell, you can see the new user was indeed saved to the database:
> use mongotest switched to db mongotest > db.accounts.find() { "username" : "randomUser", "_id" : ObjectId("538cb4cafa7c430000070f66"), "active" : false, "visits" : 0, "date_created" : ISODate("2014-06-02T17:30:50.330Z"), "__v" : 0 }
Note that without calling .save()
on the model, the changes to the model won't actually be persisted to the database. Working with Mongoose models in your node code is just that—code. You have to execute MongoDB functions on a model for any actual communication to occur with the database server.
You can use the AccountModel
to perform a find
operation and return an array of AccountModel
objects based on some search criteria that retrieve results from the MongoDB database:
// assuming our collection has the following 4 records: // { username: 'randomUser1', age: 21 } // { username: 'randomUser2', age: 25 } // { username: 'randomUser3', age: 18 } // { username: 'randomUser4', age: 32 } AccountModel.find({ age: { $gt : 18, $lt : 30} }, function(err, accounts){ console.log(accounts.length); // => 2 console.log(accounts[0].username); // => randomUser1 mongoose.connection.close(); });
Here we use the standard MongoDB $gt
and $lt
for the value of age when passing in our query parameter to find
(that is, find any document where the age is above 18 and below 30). The callback function that executes after find
references an accounts
array, which is a collection of AccountModel
objects returned from the query to MongoDB. As a general means of good housekeeping, we close the connection to the MongoDB server after we are finished.
One of the core concepts of Mongoose is that it enforces a schema on top of a schema-less design such as MongoDB. In doing so, we gain a number of new features, including built-in validation. By default, every schema type has a built-in required
validator available. Furthermore, numbers have both min
and max
validators and strings have enumeration
and matching
validators. Custom validators can also be defined via your schemas. Let's take a brief look at some validation added to our example schema from earlier:
var Account = new Schema({ username: { type: String, required: true }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false }, age: { type: Number, required: true, min: 13, max: 120 } });
The validation we added to our schema is that the username
parameter is now required, and we included a new field called age
, which is a number that must be between 13 and 120 (years). If either value doesn't match the validation requirements (that is username
is blank or age
is less than 13 or greater than 120), an error will be thrown.
Validation will fire automatically whenever a model's .save()
function is called; however, you can also manually validate by calling a model's .validate()
function with a callback to handle the response. Building on the example, add the following code that will create a new mongoose
model from the schema defined:
var AccountModel = mongoose.model('Account', Account); var newUser = new AccountModel({ username: 'randomUser', age: 11 }); newUser.validate(function(err) { console.log(err); }); // the same error would occur if we executed: // newUser.save();
Running the preceding code should log the following error to the screen:
{ message: 'Validation failed', name: 'ValidationError', errors: { age: { message: 'Path 'age' (11) is less than minimum allowed value (13).', name: 'ValidatorError', path: 'age', type: 'min', value: 11 } } }
You can see that the error object that is returned from validate
is pretty useful and provides a lot of information that can help when validating your model and returning helpful error messages back to the user.
Validation is a very good example of why it's so important to always accept an error object as the first parameter to any callback function in Node. It's equally important that you check the error object and handle appropriately.
Schemas are flexible enough so that you can easily add your own custom static methods to them, which then become available to all of your models that are defined by that schema. Static methods are great to add helper utilities and functions that you know you're going to want to use with most of your models. Let's take our simple age query from earlier and refactor it so that it's a static method and a little more flexible:
var Account = new Schema({ username: { type: String }, date_created: { type: Date, default: Date.now }, visits: { type: Number, default: 0 }, active: { type: Boolean, default: false }, age: { type: Number, required: true, min: 13, max: 120 } }); Account.statics.findByAgeRange = function(min, max, callback) { this.find({ age: { $gt : min, $lte : max} }, callback); }; var AccountModel = mongoose.model('Account', Account); AccountModel.findByAgeRange(18, 30, function(err, accounts){ console.log(accounts.length); // => 2 });
Static methods are pretty easy to implement and will make your models much more powerful once you start taking full advantage of them!
Virtual properties are exactly what they sound like—fake properties that don't actually exist in your MongoDB documents, but you can fake them by combining other real properties. The most obvious example of a virtual property would be a field for full name, when only the first and last name are actual fields in the MongoDB collection. For the full name, you simply want to say, "return the model's first and last name combined as a single string and label it fullname
":
// assuming the Account schema has firstname and lastname defined: Account.virtual('fullname') .get(function() { return this.firstname + ' ' + this.lastname; }) .set(function(fullname) { var parts = fullname.split(' '), this.firstname = parts[0]; this.lastname = parts[1]; });
Using the virtual
function of a schema, we provide the name of the property as a string. Then, we call the .get()
and .set()
functions. It's not required to provide both, although it's fairly common. Sometimes, it may be impossible to provide .set()
functionalities based on the nature of .get()
.
In this example, our get()
function simply performs basic string concatenation and returns a new value. Our .set()
function performs the reverse—splitting a string on a space and assigning the models firstname
and lastname
field values with each result. You can see that the .set()
implementation is a little flakey if someone attempts to set a model's fullname
with a value of say, Dr. Kenneth Noisewater.
There's a lot more you can do with Mongoose, and we only just barely scratched the surface. Fortunately, it has a fairly in-depth guide you can refer to at the following link:
http://mongoosejs.com/docs/guide.html
Definitely spend some time reviewing the Mongoose documentation so that you are familiar with all of the powerful tools and options available.
That concludes our introduction to Mongoose's models, schemas, and validation. Next up, let's dive back into our main application and write the schemas and models that we will be using to replace our existing sample viewModels
as well as connecting with Mongoose.
The act of connecting to a MongoDB server with Mongoose is almost identical to the method we used earlier when we used the mongodb
module.
First, we need to ensure that Mongoose is installed. At this point, we are going to be using Mongoose in our main app, so we want to install it in the main project directory and also update the package.json
file. Using your command-line terminal program, change locations to your project
folder, and install Mongoose via npm, making sure to use the --save
flag so that the package.json
file is updated:
$ cd ~/projects/imgPloadr $ npm install mongoose --save
With Mongoose installed and the package.json
file updated for the project, we're ready to open a connection to our MongoDB server. For our app, we are going to open a connection to the MongoDB server once the app itself boots up and maintain an open connection to the database server for the duration of the app's lifetime. Let's edit the server.js
file to include the connection code we need. First, include Mongoose in the app by requiring it at the very top of the file:
var express = require('express'),
config = require('./server/configure'),
app = express(),
mongoose = require('mongoose'),
Then, insert the following code right after the app = config(app);
line:
mongoose.connect('mongodb://localhost/imgPloadr'), mongoose.connection.on('open', function() { console.log('Mongoose connected.'), });
That's it! Those few simple lines of code are all it takes to open a connection to a MongoDB server, and our app is ready to start communicating with the database. The only parameter we pass to the connect
function of mongoose
is a URL string to our locally running MongoDB server and a path to the collection we want to use. Then, we add an event listener to the 'open'
event of the mongoose.connection
object and when that fires, we simply log an output message that the database server has connected.
18.191.29.151