Mongoose 101: Working with subdocuments

18th Dec 2019

You learned how to use Mongoose on a basic level to create, read, update, and delete documents in the previous tutorial. In this tutorial, we’ll go a step further into subdocuments

What’s a subdocument

In Mongoose, subdocuments are documents that are nested in other documents. You can spot a subdocument when a schema is nested in another schema.

Note: MongoDB calls subdocuments embedded documents.

const childSchema = new Schema({
  name: String
});

const parentSchema = new Schema({
  // Single subdocument
  child: childSchema,

  // Array of subdocuments
  children: [ childSchema ]
});

In practice, you don’t have to create a separate childSchema like the example above. Mongoose helps you create nested schemas when you nest an object in another object.

// This code is the same as above
const parentSchema = new Schema({
  // Single subdocument
  child: { name: String },

  // Array of subdocuments
  children: [{name: String }]
});

Updating characterSchema

Let’s say we want to create a character called Ryu. Ryu has three special moves.

  1. Hadoken
  2. Shinryuken
  3. Tatsumaki Senpukyaku

Ryu also has one ultimate move called:

  1. Shinku Hadoken

We want to save the names of each move. We also want to save the keys required to execute that move.

Here, each move is a subdocument.

const characterSchema = new Schema({
  name: { type: String, unique: true },
  // Array of subdocuments
  specials: [{
    name: String,
    keys: String
  }]
  // Single subdocument
  ultimate: {
    name: String,
    keys: String
  }
})

You can also use the childSchema syntax if you wish to. It makes the Character schema easier to understand.

const moveSchema = new Schema({
  name: String,
  keys: String
})

const characterSchema = new Schema({
  name: { type: String, unique: true },
  // Array of subdocuments
  specials: [moveSchema],
  // Single subdocument
  ultimate: moveSchema
})

Creating documents that contain subdocuments

There are two ways to create documents that contain subdocuments:

  1. Pass a nested object into new Model
  2. Add properties into the created document.

Method 1: Passing the entire object

For this method, we construct a nested object that contains both Ryu’s name and his moves.

const ryu = {
  name: 'Ryu',
  specials: [{
    name: 'Hadoken',
    keys: '↓ ↘ → P'
  }, {
    name: 'Shoryuken',
    keys: '→ ↓ ↘ → P'
  }, {
    name: 'Tatsumaki Senpukyaku',
    keys: '↓ ↙ ← K'
  }],
  ultimate: {
    name: 'Shinku Hadoken',
    keys: '↓ ↘ → ↓ ↘ → P'
  }
}

Then, we pass this object into new Character.

const char = new Character(ryu)
const doc = await char.save()
console.log(doc)
Image of Ryu's document.

Method 2: Adding subdocuments later

For this method, we create a character with new Character first.

const ryu = new Character({ name: 'Ryu' })

Then, we edit the character to add special moves:

const ryu = new Character({ name: 'Ryu' })
const ryu.specials = [{
  name: 'Hadoken',
  keys: '↓ ↘ → P'
}, {
  name: 'Shoryuken',
  keys: '→ ↓ ↘ → P'
}, {
  name: 'Tatsumaki Senpukyaku',
  keys: '↓ ↙ ← K'
}]

Then, we edit the character to add the ultimate move:

const ryu = new Character({ name: 'Ryu' })

// Adds specials
const ryu.specials = [{
  name: 'Hadoken',
  keys: '↓ ↘ → P'
}, {
  name: 'Shoryuken',
  keys: '→ ↓ ↘ → P'
}, {
  name: 'Tatsumaki Senpukyaku',
  keys: '↓ ↙ ← K'
}]

// Adds ultimate
ryu.ultimate = {
  name: 'Shinku Hadoken',
  keys: '↓ ↘ → ↓ ↘ → P'
}

Once we’re satisfied with ryu, we run save.

const ryu = new Character({ name: 'Ryu' })

// Adds specials
const ryu.specials = [{
  name: 'Hadoken',
  keys: '↓ ↘ → P'
}, {
  name: 'Shoryuken',
  keys: '→ ↓ ↘ → P'
}, {
  name: 'Tatsumaki Senpukyaku',
  keys: '↓ ↙ ← K'
}]

// Adds ultimate
ryu.ultimate = {
  name: 'Shinku Hadoken',
  keys: '↓ ↘ → ↓ ↘ → P'
}

const doc = await ryu.save()
console.log(doc)
Image of Ryu's document.

Updating array subdocuments

The easiest way to update subdocuments is:

  1. Use findOne to find the document
  2. Get the array
  3. Change the array
  4. Run save

For example, let’s say we want to add Jodan Sokutou Geri to Ryu’s special moves. The keys for Jodan Sokutou Geri are ↓ ↘ → K.

First, we find Ryu with findOne.

const ryu = await Characters.findOne({ name: 'Ryu' })

Mongoose documents behave like regular JavaScript objects. We can get the specials array by writing ryu.specials.

const ryu = await Characters.findOne({ name: 'Ryu' })
const specials = ryu.specials
console.log(specials)
Log of specials.

This specials array is a normal JavaScript array.

const ryu = await Characters.findOne({ name: 'Ryu' })
const specials = ryu.specials
console.log(Array.isArray(specials)) // true

We can use the push method to add a new item into specials,

const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.specials.push({
  name: 'Jodan Sokutou Geri',
  keys: '↓ ↘ → K'
})

After updating specials, we run save to save Ryu to the database.

const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.specials.push({
  name: 'Jodan Sokutou Geri',
  keys: '↓ ↘ → K'
})

const updated = await ryu.save()
console.log(updated)
Ryu updated with Jodan Sokutou Geri

Updating a single subdocument

It’s even easier to update single subdocuments. You can edit the document directly like a normal object.

Let’s say we want to change Ryu’s ultimate name from Shinku Hadoken to Dejin Hadoken. What we do is:

  1. Use findOne to get Ryu.
  2. Change the name in ultimate
  3. Run save
const ryu = await Characters.findOne({ name: 'Ryu' })
ryu.ultimate.name = 'Dejin Hadoken'

const updated = await ryu.save()
console.log(updated)
Ryu document with Dejin Hadoken.

If you enjoyed this article, please tell a friend about it! Share it on Twitter. If you spot a typo, I’d appreciate if you can correct it on GitHub. Thank you!

Other articles in this series

  1. Mongoose 101
  2. Mongoose 101: Working with subdocuments
  3. Mongoose 101: Population

Hold on while i sign you up…

🤗
Woohoo! You’re in!
Now, hold on while I redirect you.