click here to view as a presentation
-
Students Will Be Able To:
- Use EJS Partial views
- Define schemas for embedding Subdocuments
- Embed a Subdocument in its related document
- Setup
- Review the Starter Code
- Related Data Entities - Review
- Embedding Subdocuments
- Adding Reviews to a Movie
- Essential Questions
- Further Study
- Retrieve a Subdocument from a Mongoose Array
- Remove a Subdocument from a Mongoose Array
- Query for a Document that Contains a Certain Subdocument
-
cd
tostarter-code/mongoose-movies
folder within this lesson's folder in the class repo. -
Install the node modules:
$ npm install
-
Open the
mongoose-movies
folder in your code editor. -
Use
nodemon
to start the server.
-
As you can see, a navigation bar and a bit of styling has been added since yesterday.
-
However, the changes are more than skin deep...
- EJS Partial Templates have been implemented! Check out how:
- movies/index.ejs & movies/new.ejs are using EJS's
include
function to include header and footer "partial" templates. - Check these docs for more info.
- All
res.render()
calls are passing in atitle
property that's being used for the page title and to dynamically add anactive
class to the links in the nav bar.
- movies/index.ejs & movies/new.ejs are using EJS's
- Similar to how we previously added show functionality to the todos app, the show route/action for viewing a single movie has been implemented:
- views/index.ejs shows a "DETAILS" link that will send a request to the proper
show
route:GET /movies/:id
. - EJS tags write the movie's
_id
into thehref
attribute. - The
moviesCtrl.show
action is usingMovie.findById
method to retrieve the movie doc with an id ofreq.params.id
.
- views/index.ejs shows a "DETAILS" link that will send a request to the proper
-
As you may recall, relationships exist between entities.
-
For example, in the Mongoose Movies app, we might have the following entity relationships:
-
A Movie has many Reviews; A Review belongs to a Movie
Movie --< Review
(One-To-Many) -
A Movie has many Performers; A Performer has many Movies
Movie >--< Performer
(Many-To-Many)
-
-
A Movie has many Reviews
-
In a SQL Database, how we model data for an application is usually obvious - there isn't much flexibility, for example, there would have to be a
Review
model that maps to a reviews table. -
However, modeling data in MongoDB/Mongoose is more flexible, less strict, and left up to the developer to decide,
and
those decisions should be based on how best to implement the features of an application...
-
In most apps, a related entity, such as reviews, would likely be displayed with its parent being reviewed, in this case a movie.
-
If we stored reviews in their own collection by using a
Review
Model, we would have to make separate queries to access the reviews for the movies. But there's a more efficient way - embedding. -
With MongoDB/Mongoose, reviews are a perfect use case for embedding related data.
-
Subdocuments are very similar to regular documents.
-
The difference being that they themselves are not saved directly - they are saved when the document they are embedded within is saved.
-
Subdocuments do have their own schema though.
-
However, since subdocs are not saved to a collection, we do not compile a subdoc's schema into a Model.
-
If a schema is going to be used for embedding subdocs in just one Model, then there's no reason to put the schema in its own module.
-
In our case, it's okay to write the
reviewSchema
just above themovieSchema
in models/movie.js:const reviewSchema = new Schema({ content: String, rating: {type: Number, min: 1, max: 5, default: 5} }, { timestamps: true }); const movieSchema = new Schema({
-
With
reviewSchema
defined, we can now use it within themovieSchema
as follows:const movieSchema = new Schema({ ... nowShowing: { type: Boolean, default: false }, // reviews is an array of review subdocs! reviews: [reviewSchema] }, { timestamps: true });
-
We're now ready for the User Story...
-
AAU, I want to add a review for a movie that includes a rating
-
Step 1 is to determine the proper route.
-
Routing for a related, also called a nested, resource is slightly different. Take a look here.
-
Using the chart, the proper route for creating a review is:
POST /movies/:id/reviews
-
Importantly, the route provides to the server the
_id
of the movie that the review is being created for.
-
Step 2 calls for creating the UI - in this case, we need a form to submit the review to the server.
-
A nested resource like comments or reviews, usually has its form on the show view of its parent - you could have a dedicated view, but not today.
-
Open up movies/show.ejs...
-
Here's the form to add under the current
</section>
:</section> <!-- new markup below --> <br><br><h2>Reviews</h2> <form id="add-review-form" method="POST" action="/movies/<%= movie._id %>/reviews"> <label>Review:</label> <textarea name="content"></textarea> <label>Rating:</label> <select name="rating"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> <option value="5">5</option> </select> <input type="submit" value="Add Review"> </form>
-
A touch of styling. Update this existing CSS rule on line 68:
#new-form *, #add-review-form * { font-size: 20px; ...
and add this new CSS to the bottom:
#add-review-form { display: grid; grid-template-columns: auto auto; grid-gap: 1rem; } #add-review-form input[type="submit"] { width: 8rem; grid-column: 2 / 3; margin-bottom: 2rem; }
-
Browse to the "details" of a movie.
-
I warned you it would be ugly, but the form's
action
attribute looks pretty sweet! -
Step 3 calls for defining the route on the server...
-
To achieve max flexibility for CUD'ing the reviews resource, let's define a dedicated route module:
$ touch routes/reviews.js
and a controller module too:
$ touch controllers/reviews.js
-
Now let's require the new router in server.js:
const moviesRouter = require('./routes/movies'); // new reviews router const reviewsRouter = require('./routes/reviews');
and mount it like this:
app.use('/movies', moviesRouter); // mount the reviews router app.use('/', reviewsRouter);
-
Note that when mounting routers for nested resources we need more flexibility in our paths, so we are going to mount to the root (
/
) path.
-
Let's require the usual at the top of the router module and add our first route in routes/reviews.js:
const express = require('express'); const router = express.Router(); const reviewsCtrl = require('../controllers/reviews'); router.post('/movies/:id/reviews', reviewsCtrl.create); module.exports = router;
-
The server won't be happy until we create and export that
create
action...
-
Step 4 says to code and export the controller action.
-
Let's go to the new controllers/reviews.js:
const Movie = require('../models/movie'); module.exports = { create };
-
Above we are requiring the
Movie
model because we will need it to access the movie document to add a review to. -
Let's write the
create
function next...
-
Here's the
create
function used to add a review to a movie:function create(req, res) { Movie.findById(req.params.id, function(err, movie) { movie.reviews.push(req.body); movie.save(function(err) { res.redirect(`/movies/${movie._id}`); }); }); }
-
As you can see, we simply push in an object that's compatible with the embedded document's schema, call
save
on the parent doc, and redirect to wherever makes sense for the app.
-
All that's left is to update movies/show.ejs to render the reviews. Time permitting, let's type it in, otherwise we can copy/paste then review.
-
It's a bit large, next slide please...
<% if (movie.reviews.length) { %>
<table>
<thead>
<tr>
<th>Date</th>
<th>Review</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
<% movie.reviews.forEach(function(r) { %>
<tr>
<td><%= r.createdAt.toLocaleDateString() %></td>
<td><%= r.content %></td>
<td><%= r.rating %></td>
</tr>
<% }); %>
</tbody>
</table>
<% } else { %>
<h5>No Reviews Yet</h5>
<% } %>
-
Assuming no typos, you should be able to add reviews!
-
Let's wrap up with some essential questions before you start on the lab to practice this stuff!
-
Oh, when you get a chance, be sure to check out the
Further Study section which shows you how to:- Retrieve a subdocument embedded in a Mongoose array
- Remove a subdocument from a Mongoose array, and
- Query for a document that contains a certain subdocument!
Take a minute to review...
-
True or False: All schemas must be compiled into a Model.
-
Is it more efficient to embed or reference related data?
-
True or False: An embedded subdocument must have its
save
method called to be persisted to the database.
-
Mongoose arrays have an
id
method used to find a subdocument based on the subdoc's_id
:const reviewDoc = movieDoc.reviews.id('5c5ce1be03563ad5540e93e2');
-
Note that the string argument represents the
_id
of the review subdoc, not the movie doc.
-
Subdocuments have a
remove
method used to remove them from the array:// remove the first review subdoc movieDoc.reviews[0].remove();
-
There's an amazing syntax that you can use to query documents based upon the properties of subdocs.
-
Let's say you wanted to find all movies with a 5 rating:
Movie.find({'reviews.rating': 5}, function(err, movies) { console.log(movies); // wow! });
-
Wow!
reviews.rating
represents the array and a property on the subdocs within that array! -
Note that the dot property syntax must be enclosed in quotes.