Search code examples
node.jsmongodbmongoosemongoose-populate

How to remove object taking into account references in Mongoose Node.js?


This is my MongoDB schema:

var partnerSchema = new mongoose.Schema({
    name: String,
    products: [
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Product'
        }]
});

var productSchema = new mongoose.Schema({
    name: String,
    campaign: [
        {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Campaign'
        }
    ]
});

var campaignSchema = new mongoose.Schema({
    name: String,
});


module.exports = {
    Partner: mongoose.model('Partner', partnerSchema),
    Product: mongoose.model('Product', productSchema),
    Campaign: mongoose.model('Campaign', campaignSchema)
}

And I wondering how can I remove object from any document taking into account references (maybe should I use somehow populate from mongoose)? For example if I will remove Product then I assume that I will remove also ref ID in Partner and all Campaigns which belong to this Product.

At the moment I removing in this way:

var campSchema = require('../model/camp-schema');

    router.post('/removeProduct', function (req, res) {
            campSchema.Product.findOneAndRemove({ _id: req.body.productId }, function (err, response) {
                if (err) throw err;
                res.json(response);
            });
        });

However in mongo still left references.


Solution

  • You would have to nest your calls to remove the product id from the other model. For instance, in your call to remove the product from the Product collection, you could also make another call to remove the ref from the Partner model within the results callback. Removing the product by default will remove its refs to the Campaign Model.

    The following code shows the intuition above:

    var campSchema = require('../model/camp-schema');
    
    router.post('/removeProduct', function (req, res) {
        campSchema.Product.findOneAndRemove({ _id: req.body.productId }, function (err, response) {
            if (err) throw err;
            campSchema.Partner.update(
                { "products": req.body.productId },
                { "$pull": { "products": req.body.productId } },
                function (err, res){
                    if (err) throw err;
                    res.json(res);
                }
            );
        });
    });
    

    To remove the associated campaigns then you may need an extra remove operation that takes in the associated campaign id fro a given product id. Consider the following dirty hack which may potentially award you a one-way ticket to callback hell if not careful with the callback nesting:

    router.post('/removeProduct', function (req, res) {
        campSchema.Product.findOneAndRemove(
            { _id: req.body.productId }, 
            { new: true },
            function (err, product) {
                if (err) throw err;
                campSchema.Partner.update(
                    { "products": req.body.productId },
                    { "$pull": { "products": req.body.productId } },
                    function (err, res){
                        if (err) throw err;
                        var campaignList = product.campaign
                        campSchema.Campaign.remove({ "_id": { "$in": campaignList } })
                                    .exec(function (err, res){
                                        if (err) throw err;
                                        res.json(product);
                                    })
                    }
                );
            }
        );
    });
    

    Although it works, the above potential pitfall can be avoided by using async/await or the async library. But firstly, to give you a better understanding of the using multiple callbacks with the async module, let's illustrate this with an example from Seven Things You Should Stop Doing with Node.js of multiple operations with callbacks to find a parent entity, then find child entities that belong to the parent:

    methodA(function(a){
        methodB(function(b){
            methodC(function(c){
                methodD(function(d){
                    // Final callback code        
                })
            })
        })
    })
    

    With async/await, your calls will be restructured structured as

    router.post('/removeProduct', async (req, res) => {
        try {
            const product = await campSchema.Product.findOneAndRemove(
                { _id: req.body.productId }, 
                { new: true }
            )
    
            await campSchema.Partner.update(
                { "products": req.body.productId },
                { "$pull": { "products": req.body.productId } }
            )
    
            await campSchema.Campaign.remove({ "_id": { "$in": product.campaign } })
    
            res.json(product)
        } catch(err) {
            throw err
        }
    })
    

    With the async module, you can either use the series method to address the use of callbacks for nesting code of multiple methods which may result in Callback Hell:

    Series:

    async.series([
        function(callback){
            // code a
            callback(null, 'a')
        },
        function(callback){
            // code b
            callback(null, 'b')
        },
        function(callback){
            // code c
            callback(null, 'c')
        },
        function(callback){
            // code d
            callback(null, 'd')
        }],
        // optional callback
        function(err, results){
            // results is ['a', 'b', 'c', 'd']
            // final callback code
        }
    )
    

    Or the waterfall:

    async.waterfall([
        function(callback){
            // code a
            callback(null, 'a', 'b')
        },
        function(arg1, arg2, callback){
            // arg1 is equals 'a' and arg2 is 'b'
            // Code c
            callback(null, 'c')
        },
        function(arg1, callback){      
            // arg1 is 'c'
            // code d
            callback(null, 'd');
        }], function (err, result) {
            // result is 'd'    
        }
    )
    

    Now going back to your code, using the async waterfall method you could then restructure your code to

    router.post('/removeProduct', function (req, res) {
        async.waterfall([
            function (callback) {
                // code a: Remove Product
                campSchema.Product.findOneAndRemove(
                    { _id: req.body.productId }, 
                    function (err, product) {
                        if (err) callback(err);
                        callback(null, product);
                    }
                );
            },
    
            function (doc, callback) {
                // code b: Remove associated campaigns
                var campaignList = doc.campaign;
                campSchema.Campaign
                    .remove({ "_id": { "$in": campaignList } })
                    .exec(function (err, res) {
                    if (err) callback(err);
                    callback(null, doc);
                }
                );
            },
    
            function (doc, callback) {
                // code c: Remove related partner
                campSchema.Partner.update(
                    { "products": doc._id },
                    { "$pull": { "products": doc._id } },
                    function (err, res) {
                        if (err) callback(err);
                        callback(null, doc);
                    }
                );
            }
        ], function (err, result) {
            if (err) throw err;
            res.json(result);  // OUTPUT OK
        });
    });