Sling Academy
Home/Node.js/Mongoose: How to update a nested array in a document

Mongoose: How to update a nested array in a document

Last updated: December 30, 2023

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.

Next Article: Mongoose: $lt and $gt operators (with examples)

Previous Article: How to Set Unsigned Integer Field in Mongoose

Series: Mongoose.js Tutorials

Node.js

You May Also Like

  • NestJS: How to create cursor-based pagination (2 examples)
  • Cursor-Based Pagination in SequelizeJS: Practical Examples
  • MongooseJS: Cursor-Based Pagination Examples
  • Node.js: How to get location from IP address (3 approaches)
  • SequelizeJS: How to reset auto-increment ID after deleting records
  • SequelizeJS: Grouping Results by Multiple Columns
  • NestJS: Using Faker.js to populate database (for testing)
  • NodeJS: Search and download images by keyword from Unsplash API
  • NestJS: Generate N random users using Faker.js
  • Sequelize Upsert: How to insert or update a record in one query
  • NodeJS: Declaring types when using dotenv with TypeScript
  • Using ExpressJS and Multer with TypeScript
  • NodeJS: Link to static assets (JS, CSS) in Pug templates
  • NodeJS: How to use mixins in Pug templates
  • NodeJS: Displaying images and links in Pug templates
  • ExpressJS + Pug: How to use loops to render array data
  • ExpressJS: Using MORGAN to Log HTTP Requests
  • NodeJS: Using express-fileupload to simply upload files
  • ExpressJS: How to render JSON in Pug templates