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.
- Hadoken
- Shinryuken
- Tatsumaki Senpukyaku
Ryu also has one ultimate move called:
- 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:
- Pass a nested object into
new Model
- 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)
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)
Updating array subdocuments
The easiest way to update subdocuments is:
- Use
findOne
to find the document - Get the array
- Change the array
- 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)
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)
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:
- Use
findOne
to get Ryu. - Change the
name
in ultimate
- Run
save
const ryu = await Characters.findOne({ name: 'Ryu' }) ryu.ultimate.name = 'Dejin Hadoken' const updated = await ryu.save() console.log(updated)