Skip to content

Instantly share code, notes, and snippets.

@jim-clark
Last active February 23, 2023 08:44
Show Gist options
  • Save jim-clark/3f360aafb3cc65751dea4c02093322af to your computer and use it in GitHub Desktop.
Save jim-clark/3f360aafb3cc65751dea4c02093322af to your computer and use it in GitHub Desktop.

click here to view as a presentation


Intro To



Learning Objectives


  • Describe the use case for Mongoose

  • Define a basic Schema for a single Model

  • Create and Read documents using a Model

  • Define default values in a Schema

  • Define validations in a Schema


Roadmap


  1. Intro to Mongoose
  2. Including Mongoose in an app
  3. Defining Schemas in Mongoose
  4. Built-in Types for Properties
  5. Compiling Schemas into Models
  6. Use a Model to Create data
  7. Use a Model to Read data
  8. Defining default values for a Property
  9. Defining validations for a Property
  10. Essential Questions

Intro to Mongoose


Intro to Mongoose


  • What is Mongoose?

  • Sneak peak of some Mongoose code

  • The big picture


What is Mongoose?


Yes, this guy, but not in the context of MongoDB...


What is Mongoose?


  • Mongoose is to MongoDB as Django's ORM is to a SQL database.

  • But, because it maps code to MongoDB documents, it is referred to as an Object Document Mapper (ODM) instead of an ORM.

  • Mongoose is going to make it easier to perform CRUD using object-oriented JS instead of working directly MongoDB.


What is Mongoose? (cont.)


  • Using the Mongoose ODM is by far the most popular way to perform CRUD on a MongoDB.

  • Let's check out the landing page for Mongoose and see what it has to say for itself...

    Mongoose Homepage


What is Mongoose? (cont.)

  • So, Mongoose's homepage says it best:

"Mongoose provides a straight-forward, schema-based solution to model your application data"

  • Wait a second, what's with this "schema" business, isn't MongoDB schema-less?

  • Well, yes it is, however, the vast majority of applications benefit when their data conforms to a defined structure (schema).

  • Mongoose allows us to define schemas and ensures that documents conform.


What is Mongoose? (cont.)


  • Mongoose also provides lots of other useful functionality:
    • Default property values
    • Validation
    • Automatic related model population via the populate method
    • Virtual properties - create properties like "fullName" that are not persisted in the database
    • Custom Instance methods which operate on the document
    • Static methods which operate on the entire collection
    • pre and post event lifecycle hooks (Mongoose "middleware")

Sneak peak of some Mongoose code


  • For a preview of what Mongoose does, let's review the small amount of code shown on the Mongoose homepage...

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var Cat = mongoose.model('Cat', { name: String });

var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
  if (err) {
    console.log(err);
  } else {
    console.log('meow');
  }
});

So, the big picture is...


The Big Picture


  • Here is the big picture overview of the components we'll be working with:


Big Picture Example

  • Assuming the following schema:

     var postSchema = new mongoose.Schema({
     	content: String
     });
  • It can be compiled into a model and that model exported like this:

     module.exports = mongoose.model('Post', postSchema);
  • The model can then be required and used to perform CRUD on the posts collection in the MongoDB:

     var Post = require('./models/post');
     Post.create({content: 'Amazing post...'});

Review Questions


  • In your own words, describe the use case for Mongoose (what is it's purpose and when might you choose to use it?).

  • A Mongoose _________ is compiled into a Mongoose Model.

  • We use a Mongoose _________ to perform CRUD operations on a MongoDB..


Including Mongoose
in an App


Including Mongoose in an App


  • Create an Express app

  • Install Mongoose

  • Configure Mongoose in a module

  • Add event listeners to the Mongoose connection


Create an Express App


  • Let's use Express Generator:

     $ express first-mongoose -e

    then

     $ cd first-mongoose && npm install
  • Let's also change app.js to server.js - what do we have to do?


Install Mongoose


  • Open VS Code: $ code .

  • Installing the Mongoose package is straight forward:

     $ npm i mongoose

    Note: i is a shortcut for install


Configure Mongoose in a module


  • We're going to create a separate module named database.js and put it in a folder named config:

     $ mkdir config
     $ touch config/database.js

Configure Mongoose in a module (cont.)

  • Then in database.js, let's connect to a database named movies:

     var mongoose = require('mongoose');
     // It used to be necessary to set the mongoose library to avoid
     // warnings. Not anymore with ver. 5.x
     // mongoose.Promise = Promise;
    
     mongoose.connect('mongodb://localhost/movies',
         {useNewUrlParser: true}
     );
  • The {useNewUrlParser: true} option only avoids a deprecation warning and is not required.


Configure Mongoose in a module (cont.)


  • Time to require our database.js module in server.js, and when we do, that is when the code to connect to MongoDB runs:

     var logger = require('morgan');
     
     // connect to the database with Mongoose
     require('./config/database');

Configure Mongoose in a module (cont.)


  • Note that we aren't assigning our module to a variable. That's because there's no need to because:

    • We didn't export anything.
    • We didn't need to export anything because we will be requiring the mongoose module as needed and...
    • Since Mongoose is a singleton, changes and configuration on it is reflected everywhere we require it.

Configure Mongoose in a module (cont.)


  • Time to check if our app starts up without errors:

    • Ensure that the MongoDB engine is running in a separate Terminal session:
      $ mongod
    • Start our app:
      $ nodemon
    • Browse to:
      localhost:3000
  • No errors? Great! However, wouldn't it be nice to know that our connection to our database was successful? Sure it would...


Adding event listeners to the Mongoose connection


  • The Mongoose connection object inherits from Node's EventEmitter which allows us to listen to defined events.

  • Let's listen to the open and error events...


Adding event listeners (cont.)

  • Let's modify our database.js module as follows:

     var mongoose = require('mongoose');
     mongoose.connect('mongodb://localhost/movies');
     
     // shortcut to mongoose.connection object
     var db = mongoose.connection;
     
     db.once('open', function() {
     	console.log(`Connected to MongoDB at ${db.host}:${db.port}`);
     });
     
     db.on('error', function(err) {
     	console.error(`Database error:\n${err}`);
     });
  • Now check it out with both the MongoDB engine running and not running (to trigger an error).


Review Questions


  1. What is the advantage of creating a database.js module?

  2. What method on the Mongoose object connects to a MongoDB database?


Defining Schemas in Mongoose


Defining Schemas in Mongoose


  • Create a module for the Schema/Model

  • Define a basic Schema for a Movie model


Create a module for the Schema/Model


  • Now that we are connected to the MongoDB engine, it's time to define our first schema.

  • So, where are we going to put our app's schemas and models? In their own folder - of course!

  • The MVC design pattern influences our code organization:

     $ mkdir models
     $ touch models/movie.js

Define a basic Schema for a Movie model

  • It is customary to have a single file per Mongoose Model where we define and compile its schema.

  • In our schema/model files, we will always do this:

     var mongoose = require('mongoose');
     // optional shortcut to the mongoose.Schema class
     var Schema = mongoose.Schema;
  • Creating the shortcut to the mongoose.Schema class is optional but convenient when defining complex schemas.

  • Now let's define our schema...


Define a basic Schema (cont.)


  • Here's our basic Movie schema:

     var Schema = mongoose.Schema;
     
     var movieSchema = new Schema({
     	title: String,
     	releaseYear: Number,
     	rating: String,
     	cast: [String]
     });
  • Note the cast property's type is an Array of Strings.


Define a basic Schema (cont.)


  • Vocab note:

    • A property may be referred to as a "path", or "field".
  • YOU DO:

    • Add an additional property named nowShowing with a type of Boolean.

Define a basic Schema (cont.)


  • What we have defined is a very basic schema. Later we will see much more complex schemas.

  • For now, let's take a look at the eight built-in types available...


Built-in Types for Properties


  • The types that we can assign to properties are known as SchemaTypes

  • There are 8 built-in types that we can specify for our properties:

    • String
    • Number
    • Boolean
    • Date
    • mongoose.Schema.Types.ObjectId
    • mongoose.Schema.Types.Buffer
    • Array - []
    • mongoose.Schema.Types.Mixed

Compiling Schemas into Models


Compiling Schemas into Models


Remember - Models,
not schemas are used to perform CRUD


Compiling Schemas into Models

  • Mongoose performs CRUD using a Model.

  • Compiling a schema into a model is as easy as calling the mongoose.model method:

     var Schema = mongoose.Schema;
     	
     var movieSchema = new Schema({
     	title: String,
     	releaseYear: Number,
     	rating: String,
     	cast: [String],
     	nowShowing: Boolean
     });
     
     // Compile the schema into a model and export it
     module.exports = mongoose.model('Movie', movieSchema);

Compiling Schemas into Models


  • There is a one-to-one mapping between Mongoose models and MongoDB collections.

  • By default, the collection will be named as the pluralized version of the model in all lower-case.

  • The collection name can be overridden when compiling the model, but it's uncommon to do so.


Use a Model to Create data


Use a Model to Create data


  • Now that we have a model, we're ready to perform some CRUD!

  • First up is creating data.

  • We have two ways to create documents:

    • new <Model>(), then <model instance>.save()
    • <Model>.create()
  • Let's see how we can create a document in Node's REPL...


Use a Model to Create data (cont.)


$ node
> require('./config/database')
> var Movie = require('./models/movie')
> Movie.create({
... title: 'Star Wars',
... releaseYear: 1977
... }, function(err, doc) {
... console.log(doc);
... })
  • Logged out will be a document that looks something like...


Use a Model to Create data (cont.)

{ __v: 0,
  title: 'Star Wars',
  releaseYear: 1977,
  _id: 57ea692bab09506a97e969ba,
  cast: [] }
  • The __v field is added by Mongoose to track versioning - ignore it.

  • Note that although we did not supply a value for the cast property, it was initialized to an array - ready to have cast members pushed into it!

  • Also note that we did not provide a value for nowShowing, so it is not in our document.


Use a Model to Create data (cont.)


  • That was fun. Exit the REPL and let's see how we can use
    new + save to create movie documents - but this time from within our app.

Use a Model to Create data (cont.)


  • As we build out our CRUD functionality, here is the process we will repeat:
    1. Determine the verb + URI for the route. Use RESTful convention whenever possible.
    2. Add the UI (link and/or forms) to the view that will trigger the request.
    3. Define the route in the appropriate router module for the request, mapping it to the <controller>.<method>.
    4. Add the controller method and be sure to export it.

Use a Model to Create data (cont.)


  • We first need a route that will take us to a new.ejs view.

  • Express generator stubbed up a users.js route file, rename the file to movies.js.

  • Due to the above renaming, we'll need to make a couple of changes in server.js - what are they?


Use a Model to Create data (cont.)


  • Inside of routes/movies.js, let's code our first route - responsible for showing a form for entering a movie:

     var express = require('express');
     var router = express.Router();
     
     // GET /movies/new
     router.get('/new', function(req, res) {
     	res.render('movies/new');
     });
     
     module.exports = router;

Use a Model to Create data (cont.)


  • Now for the view.

  • As we've discussed, organizing views for a certain model into a dedicated folder makes sense:

     $ mkdir views/movies
     $ touch views/movies/new.ejs
    
  • Next, add the HTML boilerplate to new.ejs.

  • The next slide has our ugly form...


<h2>Enter a New Movie</h2>
<form action="/movies" method="post">
	<label>Title:
	  <input type="text" name="title">
	</label><br>
	<label>Release Year:
	  <input type="text" name="releaseYear">
	</label><br>
	<label>Rating
	  <select name="rating">
	    <option value="G">G</option>
	    <option value="PG">PG</option>
	    <option value="PG-13">PG-13</option>
	    <option value="R">R</option>
	  </select>
	</label><br>
	<label>Cast (separate actors with commas):
	  <input type="text" name="cast">
	</label><br>
	<label>Now Showing:
	  <input type="checkbox" name="nowShowing" checked>
	</label><br>
	<input type="submit" value="Add Movie">
</form>

Use a Model to Create data (cont.)


  • What RESTful route should we POST the form to?

  • First, let's require the movies controller like this:

     var express = require('express');
     var router = express.Router();
     // Add movies controller
     var moviesCtrl = require('../controllers/movies');
  • YOU DO: Assuming the movies controller exports a create method, write the route that will invoke it!


Use a Model to Create data (cont.)


  • Then let's create the controller for our movies resource:

     $ mkdir controllers
     $ touch controllers/movies.js
    
  • In controllers/movies.js we're going to be using our Movie model, so we need to require it:

     var Movie = require('../models/movie');

Use a Model to Create data (cont.)


  • The next slide shows how we use Mongoose in the controller to create the movie submitted by our form.

  • We'll review it as we type it...


function create(req, res) {
  // convert nowShowing's checkbox of nothing or "on" to boolean
  req.body.nowShowing = !!req.body.nowShowing;
  // remove whitespace next to commas
  req.body.cast = req.body.cast.replace(/\s*,\s*/g, ',');
  // split if it's not an empty string
  if (req.body.cast) req.body.cast = req.body.cast.split(',');
  var movie = new Movie(req.body);
  movie.save(function(err) {
    // one way to handle errors
    if (err) return res.render('movies/new');
    console.log(movie);
    // for now, redirect right back to new.ejs
    res.redirect('/movies/new');
  });
}

Use a Model to Create data (cont.)


  • You should now be able to submit movies - congrats!

  • We'll move on to displaying the movies in a bit, but first a little refactoring practice...


Practice (5 mins)

  • Refactor our earlier router.get('/new', ... by replacing the anonymous inline function with a function exported by controllers/movies.js.

  • Hint: The controller's module.exports will look something like this:

     module.exports = {
       new: newMovie,
       create: create
     };

    Note that we can't define a function named "new", because it's a reserved word in JS.


Use a Model to Read data


Use a Model to Read data


  • The querying ability of Mongoose is very capable. For example:

     Movie.find({rating: 'PG'})
     	.where('releaseYear').lt(1970)
     	.where('cast').in('John Wayne')
     	.sort('-title')
     	.limit(3)
     	.select('title releaseYear')
     	.exec(cb);
  • But we're going to start with the basics :)


Use a Model to Read data (cont.)

  • Here are the useful methods on the model for querying data:
    • find: Returns an array of all documents matching the query object

       Movie.find({rating: 'PG'}, function(err, movies) {...
    • findById: Find a document based on it's _id

       Movie.findById(req.params.id, function(err, movie) {...
    • findOne: Find the first document that matches the query object

       Movie.findOne({releaseYear: 2000}, function(err, movie) {...

Reading Data - Practice (15 min)


  • How can we find all movies documents?

  • Time for some practice!

  • Write the RESTful route, write the controller code, and create an index.ejs to display all of the movie documents in a HTML table.

  • Hint: You can use an array's join method to concatenate the names in the cast array in the view.

  • We'll review in 15 minutes.


Defining default values for a Property


Defining default values for a Property


  • Modifying the schema to add a simple default value

  • Using a function to provide a default value


Modifying the schema to add a simple default value

  • To add a default value, we need to switch from this simple property definition syntax:

     var movieSchema = new Schema({
     	title: String,
     	releaseYear: Number,
     	...
  • To this object syntax:

     var movieSchema = new Schema({
     	title: String,
     	releaseYear: {type: Number},
     	...

Modifying the schema to add a simple default value (cont.)

  • Now we can add a default key to specify a default value:

     var movieSchema = new mongoose.Schema({
       title: String,
       releaseYear: {type: Number, default: 2000},
       rating: String,
       cast: [String],
       nowShowing: {type: Boolean, default: true}
     });
  • Silly example defaulting the release year to 2000 - yes. But that's how we can add a simple default value.

  • FYI, defaults for array types will not work - they require the use of Mongoose middleware to set default values.


Modifying the schema to add a simple default value (cont.)

  • Test it out and we'll see that it doesn't work because the existence of releaseYear in the form prevents the default from being assigned.

  • We can fix this by deleting any key on req.body that is an empty string:

     if (req.body.cast) req.body.cast = req.body.cast.split(',');
     // remove empty properties
     for (var key in req.body) {
    		if (req.body[key] === '') delete req.body[key];
     }
  • Now if we fail to enter a release year, the default will be set.


Using a function to provide a default value


  • You've seen how to add a simple default value, but we can also provide a function definition.

  • The property's default would then be set to the value returned by the function!


Using a function to provide a default value (cont.)

  • For example, we can take our silly default for releaseYear and make it just as silly like this:

     var movieSchema = new mongoose.Schema({
       title: String,
       releaseYear: {
     	 type: Number,
     	 default: function() {
     		return new Date().getFullYear();
     	 }
       },
       rating: String,
       cast: [String],
       nowShowing: {type: Boolean, default: true}
     });

Of course, a named function could also be used.


Timestamps in Mongoose

  • Mongoose will add createdAt and add/update updatedAt fields if we set the timestamps option as follows in the schema:

     var movieSchema = new mongoose.Schema({
       title: String,
       releaseYear: {
     	 type: Number,
     	 default: function() {
     		return new Date().getFullYear();
     	 }
       },
       ...
     }, {
       timestamps: true
     });

Defining Validations for a Property


Defining validations for a Property


  • Validations are used to prevent bogus data from being saved in the database.

  • There are several built-in validators we can use.

  • However, endless flexibility is possible with custom asynchronous and synchronous validator functions and/or Mongoose middleware.

  • We'll keep it simple :)


Defining validations for a Property (cont.)


  • Movies should not be allowed to be created without a title. Let's make it required:

     var movieSchema = new mongoose.Schema({
       title: {
         type: String,
         required: true
       },
     ...
  • Now, if we try saving a movie without a title an error will be set.


Defining validations for a Property (cont.)


  • For properties that are of type Number, we can specify
    a min and max value:

     var movieSchema = new mongoose.Schema({
       ...
       releaseYear: {
         type: Number,
         default: function() {
           return new Date().getFullYear();
         },
         min: 1927
       },
       ...
  • No more silent movies!


Defining validations for a Property (cont.)

  • For properties that are of type String, we have:

    • enum: String must be in the provided list
    • match: String must match the provided regular expression
    • maxlength and minlength: Take a guess :)
  • Here is how we use the enum validator:

     var movieSchema = new mongoose.Schema({
       ...
       rating: {
         type: String,
         enum: ['G', 'PG', 'PG-13', 'R']
       },
       ...

Summary


  • Mongoose is the go to when it comes to working with a MongoDB.

  • We define Mongoose schemas, which are then compiled using the mongoose.model method into Models.

  • We use a Model to perform all CRUD for a given MongoDB collection.


Essential Questions


Take a couple of minutes to review before you get picked

  • True or false: A document's structure is defined in a Mongoose model.
  • Can a single Model be used to query more than one MongoDB collection?
  • What line of code would compile a drinkSchema into a model named Drink?
  • What do we export from the module that contains the schema definition and the compiled model?

References


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment