No description or website provided.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
final-code/mongoose-movies Fix day off by one issue Feb 15, 2019
starter-code/mongoose-movies Update to include compute avg rating in show.ejs Feb 9, 2019
README.md Fix day off by one issue Feb 15, 2019

README.md

click here to view as a presentation



Mongoose
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

Roadmap


  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

Setup


  • 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"' : '' %>>
     	ADD PERFORMER</a>

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');
     
     router.get('/performers/new', performersCtrl.new);
     
     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',
           performers
         });
       })
     }
  • 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
      <select>
        <% performers.forEach(function(p) { %>
          <option><%= p.name %></option>
        <% }) %>
      </select>
    </p>
    <form id="add-performer-form" action="/performers" method="POST">
      <label>Name:</label>
      <input type="text" name="name">
      <label>Born:</label>
      <input type="date" name="born">
      <input type="submit" value="Add Performer">
    </form>
    <%- 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-review-form,
     #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:

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


Creating Performers

  • In controllers/performers.js:

     module.exports = {
       new: newPerformer,
       create
     };
     	
     function create(req, res) {
       // need to "fix" date formatting to prevent day off by 1
       var s = req.body.born;
       req.body.born = 
         `${s.substr(5,2)}-${s.substr(8,2)}-${s.substr(0,4)}`;
       Performer.create(req.body, function(err, performer) {
         res.redirect('/performers/new');
       });
     }
  • 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:

     movie.save(function(err) {
       if (err) return res.redirect('/movies/new');
       // res.redirect('/movies');
       res.redirect(`/movies/${movie._id}`);
     });
  • 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 moviesCtrl.show action so that it will pass the movie with the performer documents in its cast array instead of ObjectIds:

     function show(req, res) {
       Movie.findById(req.params.id)
       .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 moviesCtrl.show, 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) {
       Movie.findById(req.params.id)
       .populate('cast').exec(function(err, movie) {
         // Performer.find({}).where('_id').nin(movie.cast)
         Performer.find({_id: {$nin: movie.cast}})
         .exec(function(err, performers) {
           console.log(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 -->
  <div>Cast:</div>
  <ul>
    <%- movie.cast.map(p => 
      `<li>${p.name} <small>${p.born.toLocaleDateString()}</small></li>`
    ).join('') %>
  </ul>
  <!-- end cast list -->
</section>
	
<!-- add to cast form below -->
<form id="add-per-to-cast" action="/movies/<%= movie._id%>/performers" method="POST">
  <select name="performerId">
    <%- performers.map(p => 
      `<option value="${p._id}">${p.name}</option>`
    ).join('') %>
  </select>
  <button type="submit">Add to Cast</button>
</form>

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

     router.post('/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,
       create,
       addToCast
     };
     
     function addToCast(req, res) {
       Movie.findById(req.params.id, function(err, movie) {
         movie.cast.push(req.body.performerId);
         movie.save(function(err) {
           res.redirect(`/movies/${movie._id}`);
         });
       });
     }

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?


References