Mongoose: How to update a nested array in a document

Updated: December 30, 2023 By: Guest Contributor Post a comment

Introduction

Manipulating data in MongoDB becomes more intricate as our models’ structure grows in complexity. A common scenario includes managing nested arrays within documents—arrays that hold critical sets of data relative to their parent documents. For developers working with Mongoose, the Node.js ODM library for MongoDB, updating these nested structures requires specific tactics to ensure accuracy and performance. In this tutorial, we’ll go through various ways to update a nested array using Mongoose, leveraging the latest capabilities of Node.js and JavaScript/TypeScript.

Preliminary Setup

Before diving into the update operations, we assume that you have Node.js installed and a MongoDB instance to connect to. The examples will use ES modules and the modern async/await syntax. Ensure that your `package.json` file includes the type module key-value pair:

{
  "type": "module"
}

Now, let’s define a Mongoose model with a nested array that will be used throughout our examples:

import mongoose from 'mongoose';
const { Schema } = mongoose;

const childSchema = new Schema({
  _id: false,
  name: String,
  age: Number
});

const parentSchema = new Schema({
  children: [childSchema]
});

const Parent = mongoose.model('Parent', parentSchema);

Basic Array Update

To get started with the basics, assume we already have a document with children, and we want to update one of them:

await Parent.updateOne(
  {
    _id: parentId,
    'children._id': childId
  },
  {
    $set: { 'children.$[elem].name': 'New Name' }
  },
  {
    arrayFilters: [{ 'elem._id': childId }]
  }
);

In this case, `parentId` is the ID of the parent document and `childId` is the ID of the nested object we aim to update. The `arrayFilters` option allows us to define filters to specify which elements to update in the array.

Adding to a Nested Array

More often than not, we want to add new elements to our nested array. Here’s how we might do that:

await Parent.updateOne(
  { _id: parentId },
  {
    $push: { children: { name: 'New Child', age: 5 } }
  }
);

This will add a new child object to the existing `children` array of the specified parent document.

Advanced Array Updates

When dealing with more complex scenarios, such as updating multiple children meeting certain criteria, we might have to use aggregation pipelines for updates (this feature is available from MongoDB v4.2 onwards):

await Parent.updateOne(
  { _id: parentId },
  [{
    $set: {
      children: {
        $map: {
          input: '$children',
          as: 'child',
          in: {
            $mergeObjects: [
              '$child',
              {
                name: {
                  $cond: [
                    { $gt: ['$child.age', 10] },
                    'Updated Name',
                    '$child.name'
                  ]
                }
              }
            ]
          }
        }
      }
    }
  }]
);

This operation uses the `$set` stage of the aggregation pipeline to map through each child in `children` array, and conditionally updates the `name` field for children over the age of 10.

In some advanced use cases, it’s necessary to remove elements from a nested array based on a condition. Let’s remove any child whose `name` is ‘Old Name’:

await Parent.updateOne(
  { _id: parentId },
  {
    $pull: { children: { name: 'Old Name' } }
  }
);

This will iterate through the `children` array and remove every instance where the `name` matches our condition.

Handling Transactions

When updating multiple documents or ensuring that updates are fully complete before committing, transactions are vital:

const session = await mongoose.startSession();

session.startTransaction();
try {
  await Parent.updateOne(
    { _id: parentId },
    {$push: {children: {name: 'New Child', age: 5}}},
    {session}
  );

  // Add more operations if needed

  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

This ensures that we maintain the consistency of our document writes and updates within the transaction.

Some Considerations

Using Asynchronous Operations

When updating multiple parents or their embedded children, asynchronous control flow patterns are essential. Using `async/await` not only results in clearer code but also helps in handling complex logic where you might need to perform sequential or parallel updates depending on the situation.

Best Practices and Pitfalls

Dealing with nested updates is powerful but comes with considerations:

  • Be precise with update conditions to prevent unintended modifications.
  • Consider performance implications when updating large and deeply nested arrays.
  • Remember the limits of operations in MongoDB and validate your updates.

Conclusion

In summary, Mongoose provides a rich set of methods to handle nested arrays. Whether it’s leveraging basic updates or orchestrating transactions, mastering these updates is essential for the modern developer working with complex data structures. With careful application of the methods shown in this tutorial, from basic to advanced techniques, you’ll have a solid foundation for managing and updating nested structures using Mongoose and MongoDB in a Node.js environment.