Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
664 lines (476 sloc) 16.1 KB

click here to view as a presentation

Referencing Related Data

Learning Objectives

Students Will Be Able To:

  • Use Referencing to Relate Data
  • "Populate" Referenced Documents
  • Explain the Difference Between 1:M & M:M Relationships
  • Perform CRUD Using Mongoose Models in a Node REPL


  1. Setup
  2. Review the Starter Code
  3. Use a Node REPL session to perform CRUD using Mongoose Models
  4. Create the Performer Model
  5. Creating Performers
  6. AAU, when viewing a movie's detail page, I want to see a list of the current cast and add a new performer to the list
  7. Essential Questions


  • cd to starter-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.

  • Browse to localhost:3000

Review the Starter Code

  • Today's starter code is the final code from yesterday's Mongoose - Embedding Related Data lesson with a couple of changes...

  • The cast property on the Movie model has been removed and all related forms/views and controller code have been adjusted accordingly. This was done so that in this lesson we can reference performer documents created using a Performer Model.

  • The movies/show.ejs view shows how you can use EJS to calculate an average rating for a movie.

Perform CRUD Using
Mongoose Models in a Node REPL

  • Because of the major refactor, we will want to start fresh by deleting the existing movie documents.

  • This provides an excellent opportunity to show you how to perform CRUD operations in Terminal using a Node REPL session.

  • What we are about to do will really come in handy in the future!

  • Start by opening a terminal session and make sure that you are in the mongoose-movies folder.

Perform CRUD Using
Mongoose Models in a Node REPL

  • Start a Node REPL:

     $ node
  • Connect to the MongoDB database:

     > require('./config/database')
     > Connected to MongoDB at localhost:27017
     // Press enter to return to the prompt

Perform CRUD Using
Mongoose Models in a Node REPL

  • Load the Movie Model:

     > const M = require('./models/movie')
  • Curious what the Movie Model looks like?

     > M
     // a big object...
  • Important: If you make any changes to the Model, you'll have exit Node and start again.

Perform CRUD Using
Mongoose Models in a Node REPL

  • Log all movie docs:

     > M.find({}, (e, movies) => {
     ... console.log(movies)
     ... })

    The find method returns a Query object that is first logged, followed by the movie docs. Press enter to return to the prompt.

Perform CRUD Using
Mongoose Models in a Node REPL

  • Anything that can be done with a Model in the app, can be done in the REPL including CRUD operations, manipulate individual documents, etc.

  • Next, let's remove all existing movie documents...

Perform CRUD Using
Mongoose Models in a Node REPL

  • Here's a way to delete all documents from a collection:

     > M.deleteMany({}, (err, result) => console.log(result))
     > { n: 3, ok: 1, deletedCount: 3 }
  • The empty query object provided as the first argument matches all documents, so all documents were removed.

  • Press control + C twice to exit the REPL.

Create the Performer Model

  • We would like to have a performers collection containing performer docs that can be referenced via their ObjectId in any other document.

  • First, we need to create a module for the Performer Model:

     $ touch models/performer.js

Create the Performer Model

  • We'll review the schema for the Performer Model as we type it:

     var mongoose = require('mongoose');
     var Schema = mongoose.Schema;
     var performerSchema = new Schema({
       name: {type: String, required: true, unique: true},
       born: Date
     }, {
       timestamps: true
     module.exports = mongoose.model('Performer', performerSchema);
  • We want to try to prevent duplicate performers (more on this in a bit).

Referencing Performer in Movie

  • With the Performer Model created, we can now add back the cast property in Movie:

     reviews: [reviewSchema],
     // don't forget to add a comma above
     cast: [{type: Schema.Types.ObjectId, ref: 'Performer'}]
  • The property type of ObjectId is always used to implement referencing.

  • The ref: 'Performer' is optional, but allows us to use a magical Mongoose method - populate.

Referencing Performer in Movie

  • Unlike in a Relational DB, all it takes to implement a
    one-to-many or a many-to-many relationship, is a single property of type Array.

  • It's the application logic (the developer) that determines what the cardinality will be between any two data entities.

Referencing Performer in Movie

  • In the case of mongoose-movies, we have this relationship:

    A Movie has many Performers; A Performer has many Movies

    Movie >--< Performer (Many-To-Many)

Referencing Performer in Movie

  • The thing that's different between a 1:M and a M:M relationship is that:

    • In a 1:M relationship, each of the MANY documents belongs to only ONE document. Each time we add another document to the many collection, it has to be created.
    • In a M:M relationship, we need to reference an existing document if it exists, and only create a new document if this is the first of its kind.
  • What this means for mongoose-movies is that we only want to create a new performer if they don't already exist.

AAU, I want to create a new performer if they don't already exist

  • Here's the flow we've now followed several times when adding functionality to the app:

    • Identify the "proper" Route (Verb + Path)

    • Create the UI that issues a request that matches that route.

    • Define the route on the server and map it to a controller action.

    • Code and export the controller action.

    • res.render a view in the case of a GET request, or res.redirect if data was changed.

Creating Performers - Step 1

  • If we want to show a dedicated page for adding a performer, creating one will be a two step process...

  • YOU DO: Pair up and take a minute to determine what the proper routes are for:

    • Showing a new view for entering a performer
    • Creating a new performer

Creating Performers - Step 1

  • Just like with reviews, the new performers resource means a new set of dedicated modules:

     $ touch routes/performers.js

    and a controller module too:

     $ touch controllers/performers.js

Creating Performers - Step 1

  • Again, just like with reviews, we need to require the router for performers:

     var reviewsRouter = require('./routes/reviews');
     // new performers router
     var performersRouter = require('./routes/performers');

    and mount it like this:

     app.use('/', reviewsRouter);
     // mount the performersRouter router
     app.use('/', performersRouter);

Creating Performers - Step 2

  • We need UI...

  • Let's start by adding a new link in the nav bar in partials/header.js:

     <img src="/images/camera.svg">
     <!-- new menu link below -->
     <a href="/performers/new"
     	<%- title === 'Add Performer' ? 'class="active"' : '' %>>

Creating Performers - Step 3

  • Clicking the ADD PERFORMER link is going to make a GET /performers/new request - now we need a route to map that HTTP request to code in routes/performers.js:

     var express = require('express');
     var router = express.Router();
     var performersCtrl = require('../controllers/performers');
     module.exports = router;
  • Once again, the server won't run until we create and export that new action...

Creating Performers - Step 4

  • Inside of controllers/performers.js we go:

     var Performer = require('../models/performer');
     module.exports = {
       new: newPerformer
     function newPerformer(req, res) {
       Performer.find({}, function(err, performers) {
         res.render('performers/new', {
           title: 'Add Performer',
  • Note that we will want to show the existing performers.

Creating Performers

  • We need that folder and file for the new view:

     $ mkdir views/performers
     $ touch views/performers/new.ejs
  • The next slide has the markup...

Creating Performers

  • Here's the markup for performers/new.ejs:

    <%- include('../partials/header') %>
    <p>Please first ensure that the Performer is not in the dropdown
        <% performers.forEach(function(p) { %>
          <option><%= %></option>
        <% }) %>
    <form id="add-performer-form" action="/performers" method="POST">
      <input type="text" name="name">
      <input type="date" name="born">
      <input type="submit" value="Add Performer">
    <%- include('../partials/footer') %>

Creating Performers - CSS

  • Find and update in public/stylesheets/style.css:

     #new-form *,
     #add-review-form *,
     #add-performer-form * {
       font-size: 20px;
     #add-performer-form {
       display: grid;
     #add-review-form input[type="submit"],
     #add-performer-form input[type="submit"] {
       width: 10rem;

Creating Performers

  • The action & method on the form look good, we just need to listen to that route.

  • The route to map the form submittal to the create action looks like this in routes/performers.js:

     // new route below'/performers', performersCtrl.create);
  • What's next?

Creating Performers

  • In controllers/performers.js:

     module.exports = {
       new: newPerformer,
     function create(req, res) {
       // need to "fix" date formatting to prevent day off by 1
       var s = req.body.born;
       req.body.born = 
       Performer.create(req.body, function(err, performer) {
  • Okay, give a whirl and fix those typos :)

AAU, after adding a movie, I want to see its details page

  • This user story can be accomplished with a quick refactor in the moviesCtrl.create action in controllers/movies/js: {
       if (err) return res.redirect('/movies/new');
       // res.redirect('/movies');
  • Don't forget to replace the single-quotes with back-ticks!

  • User story done! What's next?

AAU, when viewing a movie's detail page,
I want to see a list of the current cast and add a new performer to the list

  • Let's identify the steps it's going to take to implement this user story:

    • In movies/show.ejs, iterate over the movie's cast and use EJS to render them.
    • Hold it! There are ObjectIds in a movie's cast array - not subdocs. That's because we are using referencing. Oh wait, this is what the magical populate method is for!
    • In addition to the current cast, we will need a form to add a performer, so we will have to pass performers to show.ejs too - but only the performers not already in the cast.
  • Let's get started!

Replacing ObjectIds with the Actual Docs

  • Let's refactor the action so that it will pass the movie with the performer documents in its cast array instead of ObjectIds:

     function show(req, res) {
       .populate('cast').exec(function(err, movie) {
         res.render('movies/show', { title: 'Movie Detail', movie });
  • populate, the unicorn of Mongoose...

Replacing ObjectIds with the Actual Docs

  • We can chain the populate method after any query.

  • When we "build" queries like this, we need to call the exec method to actually run it (passing in the call back to it).

  • Does anyone remember how Mongoose knows to replace these ObjectIds with Performer documents?

Passing the Performers

  • While we're in, let's see how we can query for just the performers that are not in the movie's cast array.

  • First, we're going to need to access the Performer model, so require it at the top:

     var Movie = require('../models/movie');
     // require the Performer model
     var Performer = require('../models/performer');
  • Now we're ready to refactor the show action...

Passing the Performers

  • We'll review as we refactor the code:

     function show(req, res) {
       .populate('cast').exec(function(err, movie) {
         // Performer.find({}).where('_id').nin(movie.cast)
         Performer.find({_id: {$nin: movie.cast}})
         .exec(function(err, performers) {
           res.render('movies/show', {
             title: 'Movie Detail', movie, performers

    The log will show we are retrieving the performers - a good sign at this point.

Refactor show.ejs

  • The next slide has some refactored markup in movies/show.ejs.

  • It's a bit complex, so we'll review it while we make the changes.

  • We'll have to be careful though...

  <div><%= movie.nowShowing ? 'Yes' : 'Nope' %></div>
  <!-- start cast list -->
    <%- => 
      `<li>${} <small>${p.born.toLocaleDateString()}</small></li>`
    ).join('') %>
  <!-- end cast list -->
<!-- add to cast form below -->
<form id="add-per-to-cast" action="/movies/<%= movie._id%>/performers" method="POST">
  <select name="performerId">
    <%- => 
      `<option value="${p._id}">${}</option>`
    ).join('') %>
  <button type="submit">Add to Cast</button>

Refactor show.ejs - CSS

  • Add this tidbit of CSS to clean up the cast list:

     ul {
       margin: 0 0 1rem;
       padding: 0;
       list-style: none;
     li {
       font-weight: bold;

Need a Route for the Add to Cast Form Post

  • The route is RESTful, but we have to use a non-RESTful name for the controller action.

  • In routes/performers.js'/movies/:id/performers', performersCtrl.addToCast);

    addToCast - not a bad name!

The addToCast Controller Action

  • Let's write that addToCast action in controllers/performers.js:

     var Performer = require('../models/performer');
     // add the Movie model
     var Movie = require('../models/movie');
     module.exports = {
       new: newPerformer,
     function addToCast(req, res) {
       Movie.findById(, function(err, movie) {
         movie.cast.push(req.body.performerId); {

We Did It!

  • That was fun!

  • A few questions, then on to the lab!

Essential Questions

Take a couple of minutes to review...

  • What property type is used to reference other documents?

  • Describe the difference between 1:M & M:M relationships.

  • What's the name of the method used to replace an ObjectId with the document it references?