Welcome back! This is the second part to a two-part series discussing data modeling in NodeJS with Mongoose and MongoDB. In our first part we deployed a CenturyLink Cloud Compute server with MongoDB and created a new application with Mongoose and one object model. We will continue to build upon that work in this tutorial. The end result can be found on GitHub. Please download it so you can follow along.

Now that we have a functioning model object we can use to represent our collections we need the ability to create relationships between our models and define schemas. Finally we'll discuss common data modeling patterns in JSON with examples using our object models.

Schema and Validation

Document-based NoSQL databases like MongoDB are highly flexible. The database itself does not impose any type of constraints: documents are stored as is. This flexibility is great for getting something off the ground fast, but as the properties of your data evolve, there is a danger in creating fragmented documents and requiring a growing number of exceptions in your models to fill in the gaps.

By creating a schema for your data, you document the structure of your objects and provide a comprehensive plan for data validation. A few minutes up front will save you hours of frustration down the line.

While it's possible to build model data validators from scratch, Mongoose comes with a full-featured validation system. Let's expand the data models in schema.js to take advantage of them.

First, take a look at this expanded version of movieSchema. It contains validation for the year of the movie and movie ratings. For the year of the movie's release, we want to make sure that it is an integer and that it's not before 1890. The movie ratings are based on those used by the Motion Picture Association of America.

var movieSchema = new Schema ({
  title: String,
  year: {
    type: Number,
    min: 1890,
    get: v => Math.round(v),
    set: v => Math.round(v)
  },
  actors: [Schema.Types.ObjectId],
  rating: {
    enum: ['G', 'PG', 'PG-13', 'R', 'NC-17', 'X', 'Not rated' ]
  }
})

Model Relationships

Another important aspect of data modeling is the ability to create relationships between different documents across collections. Let's create a schema for an "actor" document, so we can create an association between two different models.

nodejs

There are a number of different ways to create relationships between MongoDB documents, but the easiest is to simply store object IDs. Remember, however, that NoSQL databases do not typically feature strong automatic relationship mapping.

Add a new schema to schema.js using the following code.

var actorSchema = new Schema ({
  lastName: {
    type: String,
    required: true
  },
  firstName: String,
  birthday: Date,
  imageUrl: String,
  movies: [Schema.Types.ObjectId]
});

Next, update the module.exports function to look like the following example.

// Takes a mongoose DB as argument.
module.exports = function (db) {
  assert.notEqual(null, db);

  // Create methods here for schema objects.
  movieSchema.methods = {
    getActors: function () {
      return Actor.find({_id: {'$in': this.actors}});
    },

    addActor: function (actor) {
      this.actors.push(actor._id);
    }
  };

  actorSchema.methods = {
    getMovies: function () {
      return Movie.find({_id: {'$in': this.movies}});
    },

    addMovie: function (movie) {
      this.movies.push(movie._id);
    }
  };

  // Add your schema models to Mongoose.
  this.Movie = db.model('Movie', movieSchema);
  this.Actor = db.model('Actor', actorSchema);

  return this;
};

Using this method, you can create relationships between any documents in the same MongoDB database. However, the burden of managing these relationships falls solely on your application. There are no cascading updates or deletes in a NoSQL database.

When designing your data models take the following into consideration:

  • How often is the document likely to change?
  • Do increased database operations hurt the user experience?
  • Will I be using server side caching?

Using Your Models

In this example, you can see how models are used with Mongoose and ES6-style promises to add documents to your MongoDB database.

function setup () {
  var promiseArray = [];

  console.log('Creating database records');

  // This object represents one movie.
  var tremorsMovie = new schema.Movie ({
    title: 'Tremors',
    year: 1990,
    rating: 'PG-13'
  });

  // These are the amazing actors from that movie.
  var actors = [
    new schema.Actor ({
      lastName: 'Bacon',
      firstName: 'Kevin'
    }),
    new schema.Actor ({
      lastName: 'Ward',
      firstName: 'Fred'
    }),
    new schema.Actor ({
      lastName: 'Carter',
      firstName: 'Finn'
    })
  ];

  // Create an array of promises to write actors to the DB.
  promiseArray = tremorsMovie.save()
    .then(function (movie) {
      var promises = [];

      console.log('Created movie record for', movie.title);

      actors.forEach(function (actor) {
        actor.addMovie(movie);
        promises.push(actor.save());
      });

      return promises;
    });

  // This chain eventually returns our movie object, saved to the DB.
  return Promise.all(promiseArray)
    .then(function (actors) {
      actors.forEach(function (actor) {
        console.log('Finishing actor record for', actor.firstName, actor.lastName);
        tremorsMovie.addActor(actor);
      });

      return tremorsMovie.save();
    });
};

Next Steps

Design your data model, enforce it with a schema validator, and build sensible data relationships for your application. Doing these three things up front will save you later heartache by keeping your data consistent.

For further practice with expanding data models, write an application to take JSON data from the Open Movie Database, validate and save it in MongoDB.