{ Mongoose Associations. }

Objectives:

By the end of this chapter, you should be able to:

  • Compare and contrast embedding and referencing in Mongo
  • Add references to Mongoose models
  • Build a CRUD app with references between models

Associations Intro

When we start building larger applications, we will need to create additional models. Many times, we will want to connect or associate data between one or more models. While this is a foundational part of relational databases, it is implemented a bit differently with non relational databases like MongoDB. There are two different ways to associate data with MongoDB: referencing and embedding.

Embedding

The idea with embedding is that we place data we want to associate inside of an existing document. Let's imagine we wanted to create an application where we can create authors and books for each author. With embedding, our documents might look like this

{
  id: 1,
  name: 'JK Rowling',
  countryOfOrigin: 'England',
  books: [{
    title: "Harry Potter and the Sorcerer's Stone",
    }, {
    title: "Harry Potter and the Goblet of Fire",
    }]
}

This is an example of a one-to-many relationship: one authoer has many books. To read more about embedding in mongo, read through this tutorial.

Referencing

When referencing, we use two separate collections for managing our data. In the example above, we would have a collection for authors and a collection for books.

// authors
{
  id: 1,
  name: 'JK Rowling',
  countryOfOrigin: 'England'
}
// books
{
  id: 1,
  author_id: 1,
  title: "Harry Potter and the Sorcerer's Stone",
}

{
  id: 2,
  author_id: 1,
  title: "Harry Potter and the Goblet of Fire",
}

You can read more about referencing here. We'll see a more explicit example of referencing in just a minute.

So which one?

In general, it's best to use embedding when you have smaller subdocuments and you want to read information quickly (since you do not need an additional collection query). Ideally you embed when data does not change frequently and when your documents do not grow frequently.

Referencing is useful when you have large subdocuments that change frequently and need faster writes (since you have a single collection and do not need a nested query). Referencing is also useful when you want to exclude parent data (show all the books, but don't worry about the authors of each one).

You can read more about how to think about which to choose here, here, and here.

Building off our pets app

With Mongoose, we can easily embed and reference, but what makes Mongoose really helpful is that when we choose to reference, we can easily populate documents with their subdocuments without writing more complex mongo queries. If we correctly set up our schema, we will be able to easily query across multiple collections.

Let's revisit our previous pets application and create a new model called owner. Each owner will have an array of pets and each pet will have a single owner.

Let's look at our models/owners.js

var mongoose = require("mongoose");
var ownerSchema = new mongoose.Schema({
    name: String,
    // every owner should have an array called pets
    pets: [{
      // which consists of a bunch of ids (we will use mongoose to populate the entire pet object, let's just store the id for now)
      type: mongoose.Schema.Types.ObjectId,
      // make sure that we reference the Pet model ('Pet' is defined as the first parameter to the mongoose.model method)
      ref: 'Pet'
    }]
});

var Owner = mongoose.model('Owner',ownerSchema);

module.exports = Owner;

Let's look at our models/pets.js

var mongoose = require("mongoose");
var petSchema = new mongoose.Schema({
    name: String,
    // let's create a reference to the owner model
    owner: {
      // the type is going to be just an id
      type: mongoose.Schema.Types.ObjectId,
      // but it will refer to the Owner model (the first parameter to the mongoose.model method)
      ref: 'Owner'
    }
});

var Pet = mongoose.model('Pet',petSchema);

module.exports = Pet;

RESTful routing for nested resources

Since we now have two resources, pets and owners and pets are dependent on owners (we can't have a pet without an owner), we need to nest or place our pets routes inside of owners. Here is what the RESTful routes for each resource will look like:

Owners

HTTP Verb Path Description
GET /owners Show all owners
GET /owners/new Show a form for creating a new owner
GET /owners/:id Show a single owner
GET /owners/:id/edit Show a form for editing a owner
POST /owners Create a owner when a form is submitted
PATCH /owners/:id Edit a owner when a form is submitted
DELETE /owners/:id Delete a owner when a form is submitted

Pets

HTTP Verb Path Description
GET /owners/:owner_id/pets Show all pets for an owner
GET /owners/:owner_id/pets/new Show a form for creating a new pet for an owner
GET /owners/:owner_id/pets/:id Show a single pet for an owner
GET /owners/:owner_id/pets/:id/edit Show a form for editing an owner's pet
POST /owners/:owner_id/pets Create a pet for an owner when a form is submitted
PATCH /owners/:owner_id/pets/:id Edit an owner's pet when a form is submitted
DELETE /owners/:owner_id/pets/:id Delete an owner's pet when a form is submitted

As you can see, we will have quite a few more routes so this is where having a file for each resource is quite helpful! If you are dealing with nested routes, in the file you are placing nested routes, it is important to include the following when creating the express router:

var express = require("express");
var router = express.Router({mergeParams: true}); // this is essential when working with nested routes 

CRUD with nested resources

Let's now examine what it looks like to save a pet:

router.post('/owners/:owner_id/pets', function(req, res, next){
  var fido = new Pet(req.body);
  fido.owner = req.params.owner_id;
  fido.save().then(function(pet) {
    db.Owner.findById(req.params.owner_id).then(function(owner){
      owner.pets.push(fido)
      owner.save().then(function(owner) {
        res.redirect(`/owners/${owner.id}/pets`);
      });
    });
  });
});

How about finding all of the pets for an owner? Here we need to make use of a mongoose method called populate, which will populate the entire object for one or more object id's

router.get('/owners/:owner_id/pets', function(req, res, next){
  db.Owner.findById(req.params.owner_id)
    .populate('pets')
    .exec()
    // owner now has a property called pets which is an array of all the populated pet objects! 
    .then(function(owner){
      res.render('pets/index', {owner});
  });
});

Example Application

You can see a sample CRUD application with associations here.

When you're ready, move on to Mongo and Mongoose Exercises

Continue