click here to view as a presentation
-
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
- Intro to Mongoose
- Including Mongoose in an app
- Defining Schemas in Mongoose
- Built-in Types for Properties
- Compiling Schemas into Models
- Use a Model to Create data
- Use a Model to Read data
- Defining default values for a Property
- Defining validations for a Property
- Essential Questions
-
What is Mongoose?
-
Sneak peak of some Mongoose code
-
The big picture
Yes, this guy, but not in the context of MongoDB...
-
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.
-
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...
- 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.
- 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
andpost
event lifecycle hooks (Mongoose "middleware")
- 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...
- Here is the big picture overview of the components we'll be working with:
-
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...'});
-
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..
-
Create an Express app
-
Install Mongoose
-
Configure Mongoose in a module
-
Add event listeners to the Mongoose connection
-
Let's use Express Generator:
$ express first-mongoose -e
then
$ cd first-mongoose && npm install
-
Let's also change
app.js
toserver.js
- what do we have to do?
-
Open VS Code:
$ code .
-
Installing the Mongoose package is straight forward:
$ npm i mongoose
Note:
i
is a shortcut forinstall
-
We're going to create a separate module named
database.js
and put it in a folder namedconfig
:$ mkdir config $ touch config/database.js
-
Then in
database.js
, let's connect to a database namedmovies
: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.
-
Time to require our
database.js
module inserver.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');
-
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.
-
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
- Ensure that the MongoDB engine is running in a separate Terminal session:
-
No errors? Great! However, wouldn't it be nice to know that our connection to our database was successful? Sure it would...
-
The Mongoose connection object inherits from Node's
EventEmitter
which allows us to listen to defined events. -
Let's listen to the
open
anderror
events...
-
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).
-
What is the advantage of creating a
database.js
module? -
What method on the Mongoose object connects to a MongoDB database?
-
Create a module for the Schema/Model
-
Define a basic Schema for a
Movie
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
-
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...
-
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.
-
Vocab note:
- A property may be referred to as a "path", or "field".
-
YOU DO:
- Add an additional property named
nowShowing
with a type ofBoolean
.
- Add an additional property named
-
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...
-
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
-
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);
-
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.
-
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...
$ 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...
{ __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.
- 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.
- As we build out our CRUD functionality, here is the process we will repeat:
- Determine the verb + URI for the route. Use RESTful convention whenever possible.
- Add the UI (link and/or forms) to the view that will trigger the request.
- Define the route in the appropriate router module for the request, mapping it to the
<controller>.<method>
. - Add the controller method and be sure to export it.
-
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 tomovies.js
. -
Due to the above renaming, we'll need to make a couple of changes in
server.js
- what are they?
-
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;
-
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>
-
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 acreate
method, write the route that will invoke it!
-
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 ourMovie
model, so we need to require it:var Movie = require('../models/movie');
-
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');
});
}
-
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...
-
Refactor our earlier
router.get('/new', ...
by replacing the anonymous inline function with a function exported bycontrollers/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.
-
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 :)
- Here are the useful methods on the model for querying data:
-
find
: Returns an array of all documents matching the query objectMovie.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 objectMovie.findOne({releaseYear: 2000}, function(err, movie) {...
-
-
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.
-
Modifying the schema to add a simple default value
-
Using a function to provide a 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}, ...
-
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.
-
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.
-
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!
-
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.
-
Mongoose will add
createdAt
and add/updateupdatedAt
fields if we set thetimestamps
option as follows in the schema:var movieSchema = new mongoose.Schema({ title: String, releaseYear: { type: Number, default: function() { return new Date().getFullYear(); } }, ... }, { timestamps: true });
-
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 :)
-
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.
-
For properties that are of type Number, we can specify
amin
andmax
value:var movieSchema = new mongoose.Schema({ ... releaseYear: { type: Number, default: function() { return new Date().getFullYear(); }, min: 1927 }, ...
-
No more silent movies!
-
For properties that are of type String, we have:
enum
: String must be in the provided listmatch
: String must match the provided regular expressionmaxlength
andminlength
: 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'] }, ...
-
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.
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 namedDrink
? - What do we export from the module that contains the schema definition and the compiled model?