{ 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 several models. While this is a foundational part of relational databases, it is implemented a bit differently with NoSQL databases like MongoDB. There are two different ways to associate data with MongoDB: embedded documents and document references.

Embedded Documents

The idea with embedded documents (also called "subdocuments") 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:

{
  _id: ObjectId('5a29b2a525cb8c4f42a8c5cc'),
  name: 'JK Rowling',
  countryOfOrigin: 'England',
  books: [
    {
      title: "Harry Potter and the Sorcerer's Stone",
      _id: ObjectId('5a2999e2a5a2fc114e9d5d9e')
    },
    {
      title: 'Harry Potter and the Goblet of Fire',
      _id: ObjectId('5a2999e9a5a2fc114e9d5d9f')
    }
  ]
};

This is an example of a one-to-many relationship: one author has many books. To read more about embedded documents in MongoDB, read through this tutorial. To learn more about traditional SQL one-to-many relationships, you can check out this section on the SQL curriculum.

Document References

To use document references, we employ two separate collections for managing our data. In the example above, we would have a collection for authors and a collection for books.

// authors collection
db.authors.findOne();
{
  _id: ObjectId('5a29b2a525cb8c4f42a8c5cc'),
  name: 'JK Rowling',
  countryOfOrigin: 'England'
}

// books collection
db.books.find({ author_id: ObjectId('5a29b2a525cb8c4f42a8c5cc') });
{
  _id: ObjectId('5a2999e2a5a2fc114e9d5d9e')
  author_id: ObjectId('5a29b2a525cb8c4f42a8c5cc'),
  title: "Harry Potter and the Sorcerer's Stone",
}
{
  _id: ObjectId('5a2999e9a5a2fc114e9d5d9f')
  author_id: ObjectId('5a29b2a525cb8c4f42a8c5cc'),
  title: "Harry Potter and the Goblet of Fire",
}

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

Embedded Docs vs References

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 can also be useful when you want to exclude parent data (show all the books, but don't worry about the authors of each one).

In our case, it might be better to model books as embedded documents since they do not change very often (i.e. more read-heavy than write-heavy) and you pretty much always want to query the author's books when you query the authors collection.

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

Pets App Expanded

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

const mongoose = require("mongoose");
const 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"
    }
  ]
});

module.exports = mongoose.model("Owner", ownerSchema);

Let's look at our models/pets.js

const mongoose = require("mongoose");
const 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"
  }
});

module.exports = mongoose.model("Pet", petSchema);

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 the pets routes inside of owners. Here is what the RESTful routes for each resource will look like:

Owners

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

Pets

HTTP Verb Path Description
GET /owners/:ownerId/pets Display all pets for an owner
GET /owners/:ownerId/pets/new Display a form for creating a new pet for an owner
GET /owners/:ownerId/pets/:petId Display a single pet for an owner
GET /owners/:ownerId/pets/:petId/edit Display a form for editing an owner's pet
POST /owners/:ownerId/pets Create a pet for an owner when a form is submitted
PATCH /owners/:ownerId/pets/:petId Edit an owner's pet when a form is submitted
DELETE /owners/:ownerId/pets/:petId 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:

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

CRUD with Nested Resources

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

router.post("/owners/:ownerId/pets", (req, res, next) => {
  // create a new Pet based on request body
  const newPet = new Pet(req.body);
  // extract ownerId from route
  const { ownerId } = req.params;
  // set the pet's owner via route param
  newPet.owner = ownerId;
  // save the newPet
  return newPet
    .save()
    .then(pet => {
      // update the owner's pets array
      return Owner.findByIdAndUpdate(
        ownerId, // query owner by route param
        /*
         Add new pet's ObjectId (_id) to set of Owner.pets.
         We use $addToSet instead of $push so we can ignore duplicates!
        */
        { $addToSet: { pets: pet._id } }
      );
    })
    .then(() => {
      return res.redirect(`/owners/${ownerId}/pets`);
    })
    .catch(err => next(err)); // pass DB errors along to error handler
});

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 ObjectIds.

router.get("/owners/:owner_id/pets", (req, res, next) => {
  return (
    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(owner => {
        return res.render("pets/index", { owner });
      })
      .catch(err => next(err)) // pass along DB errors to handler
  );
});

Read more about query population here.

Example Application

You can see a sample CRUD application with associations here.

Screencast

For more on associations with Mongoose, check out this screencast:

When you're ready, move on to Real Time Applications with Socket.io

Continue

Creative Commons License