This is the 5th in a series of posts leading up to Node.js Knockout on using Mongoose. This post was written by Node Knockout judge and Mongoose maintainer Aaron Heckmann.

Getting started with Mongoose and Node

In this post we’ll talk about getting started with Mongoose, an object modeling tool for MongoDB and node.js.

Install

We’re going to assume that you have both MongoDB and npm installed for this post. Once you have those, you can install Mongoose:

$ npm install mongoose

Hurray! Now we can simply require mongoose like any other npm package.

var mongoose = require('mongoose');

Schema definition

Though MongoDB is a schema-less database we often want some level of control over what goes in and out of our database collections. We’re confident that we’re going to be the next Netflix so we’ll need a Movie schema and a Rating schema. Each Movie is allowed to have multiple Ratings.

var Schema = mongoose.Schema;
var RatingSchema = new Schema({
    stars    : { type: Number, required: true }
  , comment  : { type: String, trim: true }
  , createdAt: { type: Date, default: Date.now }
});

So far we’ve created a Rating schema with a stars property of type Number, a comment property of type String, and a createdAt property of type Date. Whenever we set the stars property it will automatically be cast as a Number. Note also that we specified required which means validation will fail if an attempt is made to save a rating without setting the number of stars. Likewise, whenever we set the comment property it will first be cast as a String before being set, and since whitespace around comments is very uncool, we use the built-in trim setter.

Now that we’re happy with our Rating model we’ll use it within our Movie model. Each movie should have name, director, year, and ratings properties.

var MovieSchema = new Schema({
    name    : { type: String, trim: true, index: true }
  , ratings : [RatingSchema]
  , director: Schema.ObjectId
  , year    : Number
});

Here we see that ratings is set to an array of Rating schemas. This means that we’ll be storing Ratings as subdocuments on each Movie document. A subdocument is simply a document nested within another.

You might have noticed the index option we added to the name property. This tells MongoDB to create an index on this field.

We’ve also defined director as an ObjectId. ObjectIds are the default primary key type MongoDB creates for you on each document. We’ll use this as a foreign key field, storing the document ObjectId of another imaginary Person document which we’ll leave out for brevity.

TIP: Note that we needed to declare the subdocument Rating schema before using it within our Movie schema definition for everything to work properly.

This is what a movie might look like within the mongo shell (included when you install MongoDB):

{ name: 'Inception',
  year: 2010,
  ratings:
   [ { stars: 8.9,
       comment: 'I fell asleep during this movie, and yeah, you\'ve heard this joke before' },
     { stars: 9.3 } ],
  director: ObjectId("4e4b4a8b73e1d576d6a1438e") }

Now that we’ve finished our schemas we’re ready to create our movie model.

var Movie = mongoose.model('Movie', MovieSchema);

And thats it! Everything is all set with the exception of being able to actually talk to MongoDB. So let’s create a connection.

mongoose.connect('mongodb://localhost/nodeknockout', function (err) {
  if (err) return handleErrorSomehow(err);
  // ok we're set
});

Now we’re ready to create a movie and save it.

var movie = new Movie({ name: "Frankenweenie", director: anObjectId, year: 2012 });

movie.save(function (err) {
  if (err) return console.error(err); // we should handle this
});

Oh, but what about adding ratings?

Movie.findOne({ name: "Frankenweenie" }).where("year").equals(2012).exec(function (err, movie) {
  if (err) // handle this

  // add a rating
  movie.ratings.push({ stars: 9.0, comment: "it made me happy" });
  movie.save(callback);
});

To look up our movie we used Model.findOne which accepts a where clause as its first argument. We also took advantage of the Query object returned by this method to add some more sugary filtering. Finally, we called the Query’s run method to execute it.

We didn’t have to do it this way, instead you could just pass all of your where params directly as the first argument like so:

Movie.findOne({ name: "Frankenweenie", year: 2012 }, callback);

Though the first example is more verbose it highlights some of the expressive flexibility provided by the Query object returned.

Here are a couple more ways we could write this query:

Movie.where('name', /^Frankenweenie/i).where('year', 2012).limit(1).exec(callback);

Movie.find({ name: "Frankenweenie", year: { $gt: 2011, $lt: 2013 }}, null, { limit: 1 }, callback);

This is all well and good but what if we look up movies by director and year a lot and need the query to be fast? First we’ll create a static method on our Movie model:

MovieSchema.statics.byNameAndYear = function (name, year, callback) {
  // NOTE: find() returns an array and may return multiple results
  return this.find({ name: name, year: year }, callback);
}

We’ll also add a compound index on these two fields to give us a performance boost:

MovieSchema.index({ name: 1, year: 1 });

For good measure we’ll add a movie instance method to conveniently look up the director:

MovieSchema.methods.findDirector = function (callback) {
  // Person is our imaginary Model we skipped for brevity
  return this.model('Person').findById(this.director, callback);
}

Putting it all together:

Movie.byNameAndYear("Frankenweenie", 2012, function (err, movies) {
  if (err) return console.error(err); // handle this
  var movie = movies[0];
  movie.findDirector(function (err, director) {
    if (err) ...
    // woot
  })
});

Thats it for this post. For more info check out mongoosejs.com, the mongoose plugins site, the github README, or the Mongoose test directory to see even more examples.

  1. angelogeminiani reblogged this from nodeknockout
  2. nodeknockout posted this
Blog comments powered by Disqus