Pushing Mongoose documents to another document as array elements

Hi everyone,
I have two models, user.model.js and bundle.model.js. Inside the user model, I have a bundles array where I’d like to add Bundle documents. Here’s how it looks:

user.model.js

"use strict";

const mongoose = require('mongoose');

// Define the user schema
const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true,
    },

    bundles: [
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Bundle',
            require: false,
        }
    ]
});

UserSchema.set('versionKey', false);


// Export the User model
module.exports = mongoose.model('User', UserSchema);

bundle.model.js

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const bundleSchema = new Schema({
    remainingStories: { type: Number, required: true },
    activationDate: { type: Date, required: true },
}, { timestamps: { createdAt: 'purchaseDate' } });

const Bundle = mongoose.model('Bundle', bundleSchema);

module.exports = Bundle;

In my users.js I have a route that I want to use to add new bundles to a user. The expected behavior is for the new bundle to be added to the user’s bundles array and also to the bundles collection in MongoDB. For some reason, however, no matter how many times I push() new bundles to the array, there’s only ever one bundle in there. Also, those bundles are never added to the bundles collection in MongoDB. Here’s the relevant code in users.js:

const router = require('express').Router();
const User = require('../models/user.model');
const Bundle = require('../models/bundle.model');
...
router.route('/add-bundle/:id/').post((req, res) => {
    const remainingStories = Number(req.body.remainingStories);
    const activationDate = eval(req.body.activationDate);
    User.findById(
        { _id: req.params.id },
        { useFindAndModify: false })
        .exec()
        .then(user => {
            const bundle = new Bundle({
                remainingStories,
                activationDate
            });
            console.log(bundle);
            user.bundles.push(bundle);
            // console.log(user);
            res.status(200).json(user);

            user.save(() => console.log('Save successful!'))
        })
        .catch(err => res.status(400).json('Error: ' + err));
});

Any hints?

After const bundle = new Bundle({...., you also need to save the bundle, like bundle.save() I guess?

And secondly, I’m not 100% sure on this, but I think you need to push the _id of the bundle in your user.bundles array, not the whole bundle. Something like this:
users.bundle.push(bundle._id)

The reason is that you’ve defined bundles array schema as mongoose.Schema.Types.ObjectId with ref. And refs only work with _ids.

You can take a took at the mongoose docs for more info: https://mongoosejs.com/docs/populate.html

I thought this too and indeed it gets added to the bundles collection when I save, even though in the docs it says you should only save the parent not the individual sub-documents.

I used to do this too. Makes no difference, pushing overwrites what’s already in the array. The only time pushing doesn’t overwrite is when I remove the line ref: 'Bundle' from

bundles: [
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Bundle',
            require: false,
        }
    ]

I somehow got it to work as intended. New bundles are being added to the bundles collection and referenced via their ids in the user’s bundles array. Here’s what I changed.

In order to get new bundles added to the bundle collection I indeed had to call bundle.save() (contrary to what the docs have to say, unless I’ve misunderstood).
Then, I push() the newly created bundle’s id to the array in user. I then save() user. However, this alone caused an error to be generated by save() whenever I tried adding a bundle:

…ValidationError: User validation failed: bundles: Cast to [undefined] failed for value…

After scouring the internet for a while, I came upon this SO answer. Apparently, the solution was to explicitly initialize the bundles array with an empty array in the user’s add route. Essentially, adding the line bundles: [] like so:

router.route('/add').post((req, res) => {
    const username = req.body.username;
    const password = req.body.password;
    const newUser = new User({
        username,
        password,
        bundles: [],
    });

    newUser.save()
        .then(newUser => res.json(newUser))
        .catch(err => res.status(400).json('Error: ' + err));
});

Finally, the add-bundle route in users.js now looks like this:

router.route('/add-bundle/:id/').post((req, res) => {
    const remainingStories = Number(req.body.remainingStories);
    const activationDate = eval(req.body.activationDate);
    User.findById(
        { _id: req.params.id })
        .exec()
        .then(user => {
            const bundle = new Bundle({
                remainingStories,
                activationDate
            });
            bundle.save()
                .then(bundle => {
                    console.log(bundle._id);
                    l = user.bundles.push(bundle._id);

                    user.save()
                        .then((user) => res.status(200).json(user))
                        .catch(err => res.status(400).json('Error on user save: ' + err));
                }
                )
                .catch(err => res.status(400).json('Error on bundle save: ' + err));
        })
        .catch(err => res.status(400).json('Error: ' + err));
});

I thought the array has been already initialized. But yes, you needed to add the array yourself.

And as for the bundle.save thing, you’re mixing two things here, Subdocuments and Referencing. Subdocs are also called embedded docs, as the name suggests.

In short, subdoc means that the child object will live within the parent object. We don’t need to save the child in any other collection.
https://mongoosejs.com/docs/subdocs.html#what-is-a-subdocument-

And by reference means, that we will only store the _id of the child object in the parent object. And the child object itself will be saved in it’s own collection.
https://mongoosejs.com/docs/populate.html

Both approaches has pros and cons, and the answer really depends on your usecase.

You can Google mongodb subdocs vs references for more info.

1 Like

Thanks @husseyexplores. I guess in this case I’m using both referencing and subdocs?

You’re currently using referencing, since each bundle is saved in the bundles collection, and it’s ID is saved within the user.bundle array.

ref in the schema suggests that it will be a reference.
If there is no ref, the it is a subdoc, and if it is a subdoc, then you also DON’T need to do something like this: mongoose.model('Bundle', bundleSchema)

1 Like

What if I were to remove ref: 'Bundle' from the user model and keep everything else the same? What would change in this case? I’m guessing then I wouldn’t be able to use populate operations?

const bundleSchema = new Schema({
    remainingStories: { type: Number, required: true },
    activationDate: { type: Date, required: true },
}, { timestamps: { createdAt: 'purchaseDate' } });

const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true,
    },

    bundles: [bundleSchema]
});

// while adding a new bundle, no need to `new Bundle()..` just directly add the object
user.bundles.push({ remainingStories, activationDate });

You can find more details on the mongoose subdocuments docs which explains it in much more detail.

And yes, populate works for when you need to reference the docs which are saved in other collections. If you use subdocuments, you can’t use populate, because you never need to.

1 Like

So something like this

const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true,
    },
    bundles: [
        {
            type: mongoose.Schema.Types.ObjectId,
            // ref: 'Bundle',
            require: false,
        }
    ]
});

wouldn’t really make sense, since I wouldn’t be taking advantage of Mongoose’s built-in referencing capabilities and would have to search the target collection manually (here implicitly Bundles) till I find the document with the matching id.

You’re correct. If you remove the ref property and keep the type as ObjectId, then it means that this subdocument only contains IDs. And by not using ref, you can’t use populate. And then you’d have to manually search/save bundles collection and get the required document. But I honestly don’t think if this is a good idea.

MongoDB docs are great to get the idea of data modeling in noSQL databases.
Subdocuments/embedded docs: https://docs.mongodb.com/manual/tutorial/model-embedded-one-to-many-relationships-between-documents/

Referencing: https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/

1 Like