×
By the end of this chapter, you should be able to:
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.
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.
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.
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.
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);
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:
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 |
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
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 ObjectId
s.
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.
You can see a sample CRUD application with associations here.
For more on associations with Mongoose, check out this screencast:
When you're ready, move on to Real Time Applications with Socket.io