click here to view as a presentation
<style style="visibility:hidden"> .reveal li {font-size: 32px;} .reveal ul ul li, .reveal ul ol li, .reveal ol ol li, .reveal ol ul li { font-size: 28px; } </style>-
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
- Setup
- 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
-
cd
to today's folder. -
Let's use Express Generator:
$ express -e mongoose-movies
then
$ cd mongoose-movies && npm install
-
Let's also change
app.js
toserver.js
- what else do we have to do?
-
What is Mongoose?
-
Sneak peak of some Mongoose code
-
The big picture
Yes, this guy, but not in the context of MongoDB...
-
Mongoose is the most popular way to perform CRUD operations on a MongoDB database.
-
Mongoose is called an Object Document Mapper (ODM) because it maps object-oriented JavaScript to MongoDB documents.
-
Mongoose makes it easier to perform CRUD using object-oriented JavaScript instead of working directly MongoDB.
-
Let's check out the landing page for Mongoose and see what it has to say for itself...
-
According to Mongoose's homepage:
"Mongoose provides a straight-forward, schema-based solution to model your application data..."
-
Wait a minute, what's with this "schema" business, isn't MongoDB schema-less?
-
Well, yes it is, however, it turns out that 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 to them.
- 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 a document
- Static methods which operate on the entire collection
pre
andpost
event lifecycle hooks (Mongoose "middleware")
- Here is a big picture overview of the purpose of Mongoose's Schema and Model components:
-
Assuming the following schema:
const 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:const 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..
-
Install Mongoose
-
Configure Mongoose in a module
-
Add an event listener to the Mongoose connection
-
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
:const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/movies', {useNewUrlParser: true, useCreateIndex: true} );
-
The
{useNewUrlParser: true, useCreateIndex: true}
options avoid deprecation warnings.
-
In order for the code in
database.js
to run and connect to the database, we must require it inserver.js
: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're not exporting anything of use - why assign to a variable?
- Calling
require('./config/database')
is all it takes to make the code run. - We can
require
Mongoose in any module we want and it will always refer to the same configured Mongoose instance.
-
Time to check if our app starts up without errors...
-
Ensure that the MongoDB engine is running. You will have to run
mongod
in a separate terminal session if you haven't already told MongoDB to start automatically withbrew services start mongodb
.
-
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...
-
The Mongoose connection object inherits from Node's
EventEmitter
which allows us to listen to defined events. -
Let's listen to the
connected
event...
-
Let's modify our database.js module as follows:
const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/movies', {useNewUrlParser: true, useCreateIndex: true}); // shortcut to mongoose.connection object const db = mongoose.connection; db.on('connected', function() { console.log(`Connected to MongoDB at ${db.host}:${db.port}`); });
-
Check for the Connected to MongoDb... message in the server terminal.
-
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
- We will always have a single file per Mongoose Model where:
- We define the schema,
- Compile the schema into a model, and
- Export that model.
-
In the schema/model module, we will always do this:
const mongoose = require('mongoose'); // optional shortcut to the mongoose.Schema class const 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:
const Schema = mongoose.Schema; const movieSchema = new Schema({ title: String, releaseYear: Number, mpaaRating: String, cast: [String] });
-
Note the
cast
property's type is an Array of Strings.
-
Mongoose vocababulary:
- A property may be referred to as a "path", or "field".
-
💪 YOU DO:
- Add an additional property named
nowShowing
with a type ofBoolean
(make sure that it's uppercased so that it refers to JavaScript's built-inBoolean
object wrapper).
- Add an additional property named
-
Awesome! We have defined a Mongoose schema!
-
As we progress toward learning more about Mongoose, we will be adding more properties and functionality to the
movieSchema
. -
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 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
-
Notice that Mongoose uses a few types that are not built into JavaScript:
- mongoose.Schema.Types.ObjectId
- mongoose.Schema.Types.Buffer
- mongoose.Schema.Types.Mixed
-
When we need to specify one of the above types, e.g.,
ObjectId
, we will need to ensure that we access them through the object hierarchy. -
Defining that
Schema
shortcut variable, enables us to writeSchema.Types.ObjectId
, leaving off themongoose.
.
-
Mongoose performs CRUD using a Model.
-
Compiling a schema into a model is as easy as calling the
mongoose.model
method:const Schema = mongoose.Schema; const movieSchema = new Schema({ title: String, releaseYear: Number, mpaaRating: 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 can use a Mongoose Model in two ways to create documents in the collection:
const instance = new Model()
, theninstance.save()
, orModel.create()
-
Let's see how we can
create
a document in a Node REPL...
-
Warning, if you make a typo, you'll have to start over:
$ node > require('./config/database') > const 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...
-
Here's the newly created document:
{ __v: 0, title: 'Star Wars', releaseYear: 1977, _id: 57ea692bab09506a97e969ba, cast: [] }
-
The
__v
field is added by Mongoose to track versioning - ignore it.
-
Note that we did not provide a value for
nowShowing
so it was not created as a property in the document. -
However, properties of type Array, are always initialized to empty arrays like
cast
was. This makes it easy to start pushing performers into it!
- That was fun! Exit the REPL (
ctrl + C
twice) and let's see how we can usenew
+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 conventions 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 action/method and be sure to export it.
- In the controller, perform necessary CRUD and either
render
(passing it the data) orredirect
.
-
Using our trusty routing chart, we find that to display a
new.ejs
view with a form for entering movies, the proper route will be:GET /movies/new
-
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:const express = require('express'); const router = express.Router(); const moviesCtrl = require('../controllers/movies'); // GET /movies/new router.get('/new', moviesCtrl.new); module.exports = router;
-
💪 YOU DO: Pair up and create the controller and export the
new
action - we did this quite a bit yesterday...(hints next slide)
-
Start by:
- Creating
controllers/movies.js
- Creating
-
The
new
action is just the first of several that are going to be exported from this module. -
The code in the
new
action is pretty simple:function newMovie(req, res) { res.render('movies/new'); }
-
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
and link in:<link rel='stylesheet' href='/stylesheets/style.css' />
-
The next slide has our awesome but 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>MPAA Rating
<select name="mpaaRating">
<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>
-
Note that we've already set the
action
&method
attributes to match the proper RESTful route to submit the form to. -
Let's define that route in routes/movies.js:
router.post('/', moviesCtrl.create);
-
The next step is to write that
create
controller action...
-
In controllers/movies.js we're going to be using our
Movie
model, so we need to require it at the top:const Movie = require('../models/movie');
-
The next slide shows how we use the
Movie
Model in the controller to create the movie submitted by the form. -
We'll review it as we type it...
- Don't forget to export
create
, then write the function:
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(',');
const 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!
-
Now that we have created a movie or two, let's see how we use Mongoose models to read documents from a MongoDB collection...
-
The querying ability of Mongoose is very capable. For example:
Movie.find({mpaaRating: 'PG'}) .where('releaseYear').lt(1970) .where('cast').in('Bob Hope') .sort('-title') .limit(3) .select('title releaseYear') .exec(cb);
-
But we're going to start with the basics :)
- Here are the useful methods on a Model for querying data:
-
find
: Returns an array of all documents matching the query objectMovie.find({mpaaRating: '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) {...
-
-
💪 YOU DO - Pair up and display the list of movies!:
- Define the RESTful route
- Write the controller
index
action to read and provide all movies to the view - Create an index.ejs view to display in an HTML table.
-
Hint: In the view, use the array
join
method to concatenate the names inside of thecast
array. -
We'll review in 20 minutes.
-
Now that we have an
index
view, let's update theredirect
in thecreate
action:movie.save(function(err) { if (err) return res.render('movies/new'); console.log(movie); res.redirect('/movies'); // update this line });
-
Modify the schema to add a default value
-
Use a function to provide a default value
-
To add a default value, we need to switch from this simple property definition syntax:
const movieSchema = new Schema({ title: String, releaseYear: Number, ...
-
To this object syntax:
const movieSchema = new Schema({ title: String, releaseYear: {type: Number}, ...
-
Now we can add a
default
key to specify a default value:const movieSchema = new mongoose.Schema({ title: String, releaseYear: {type: Number, default: 2000}, mpaaRating: String, cast: [String], nowShowing: {type: Boolean, default: false} });
-
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 find that it didn't work for the
releaseYear
becausereq.body.releaseYear
exists and this prevents the default from being assigned. -
We can fix this in the
create
action by deleting any property inreq.body
that is an empty string:if (req.body.cast) req.body.cast = req.body.cast.split(','); // remove empty properties for (let key in req.body) { if (req.body[key] === '') delete req.body[key]; }
-
Now if we don't 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 as well.
-
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:
const movieSchema = new mongoose.Schema({ title: String, releaseYear: { type: Number, default: function() { return new Date().getFullYear(); } }, mpaaRating: String, cast: [String], nowShowing: {type: Boolean, default: true} });
-
Of course, named functions will work too.
-
Mongoose will add
createdAt
and add + updateupdatedAt
fields automatically to every document if we set thetimestamps
option as follows in the schema:const movieSchema = new mongoose.Schema({ ... }, { timestamps: true });
-
This really comes in handy so it's recommended to add the
timestamps: true
option to all schemas by default.
-
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 at first...
-
Movies should not be allowed to be created without a
title
. Let's make it required:const movieSchema = new mongoose.Schema({ title: { type: String, required: true }, ...
-
Now, if we try saving a movie without a
title
an error will be set and we'll render thenew
view instead of being redirected to theindex
.
-
For properties that are of type Number, we can specify
amin
andmax
value:const 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:const movieSchema = new mongoose.Schema({ ... mpaaRating: { 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 in case you get picked!
-
True or False: In our code, a document's structure is defined in a Mongoose model.
-
Name at least two Model methods used to read data from a MongoDB collection.
-
Can a single Model be used to query more than one MongoDB collection?