Cascade style delete in Mongoose

MongodbMongoose

Mongodb Problem Overview


Is there a way to delete all children of an parent in Mongoose, similar to using MySQLs foreign keys?

For example, in MySQL I'd assign a foreign key and set it to cascade on delete. Thus, if I were to delete a client, all applications and associated users would be removed as well.

From a top level:

  1. Delete Client
  2. Delete Sweepstakes
  3. Delete Submissions

Sweepstakes and submissions both have a field for client_id. Submissions has a field for both sweepstakes_id, and client_id.

Right now, I'm using the following code and I feel that there has to be a better way.

Client.findById(req.params.client_id, function(err, client) {

	if (err)
		return next(new restify.InternalError(err));
	else if (!client)
		return next(new restify.ResourceNotFoundError('The resource you requested could not be found.'));

	// find and remove all associated sweepstakes
	Sweepstakes.find({client_id: client._id}).remove();

	// find and remove all submissions
	Submission.find({client_id: client._id}).remove();

	client.remove();

	res.send({id: req.params.client_id});

});

Mongodb Solutions


Solution 1 - Mongodb

This is one of the primary use cases of Mongoose's 'remove' middleware.

clientSchema.pre('remove', function(next) {
    // 'this' is the client being removed. Provide callbacks here if you want
    // to be notified of the calls' result.
    Sweepstakes.remove({client_id: this._id}).exec();
    Submission.remove({client_id: this._id}).exec();
    next();
});

This way, when you call client.remove() this middleware is automatically invoked to clean up dependencies.

Solution 2 - Mongodb

In case your references are stored other way around, say, client has an array of submission_ids, then in a similar way as accepted answer you can define the following on submissionSchema:

submissionSchema.pre('remove', function(next) {
    Client.update(
        { submission_ids : this._id}, 
        { $pull: { submission_ids: this._id } },
        { multi: true })  //if reference exists in multiple documents 
    .exec();
    next();
});

which will remove the submission's id from the clients' reference arrays on submission.remove().

Solution 3 - Mongodb

Here's an other way I found

submissionSchema.pre('remove', function(next) {
    this.model('Client').remove({ submission_ids: this._id }, next);
    next();
});

Solution 4 - Mongodb

I noticed that all of answers here have a pre assigned to the schema and not post.

my solution would be this: (using mongoose 6+)

ClientSchema.post("remove", async function(res, next) { 
    await Sweepstakes.deleteMany({ client_id: this._id });
    await Submission.deleteMany({ client_id: this._id });
    next();
});

By definition post gets executed after the process ends pre => process => post.

Now, you're probably wondering how is this different than the other solutions provided here. What if a server error or the id of that client was not found? On pre, it would delete all sweeptakes and submissions before the deleting process start for client. Thus, in case of an error, it would be better to cascade delete the other documents once client or the main document gets deleted.

async and await are optional here. However, it matters on large data. so that the user wouldn't get those "going to be deleted" cascade documents data if the delete progress is still on.

At the end, I could be wrong, hopefully this helps someone in their code.

Solution 5 - Mongodb

Model

const orderSchema = new mongoose.Schema({
    // Множество экземпляров --> []
    orderItems: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'OrderItem',
        required: true
    }],
    ...
    ...
});

asyncHandler (optional)

const asyncHandler = fn => (req, res, next) =>
  Promise
    .resolve(fn(req, res, next))
    .catch(next)

module.exports = asyncHandler;

controller

const asyncHandler = require("../middleware/asyncErrHandler.middleware");

// **Models**
const Order = require('../models/order.mongo');
const OrderItem = require('../models/order-item.mongo');


// @desc        Delete order
// @route       DELETE /api/v1/orders/:id
// @access      Private
exports.deleteOrder = asyncHandler(async (req, res, next) => {
    let order = await Order.findById(req.params.id)

    if (!order) return next(
        res.status(404).json({ success: false, data: null })
    )

    await order.remove().then( items => {
        // Cascade delete -OrderItem-
        items.orderItems.forEach( el => OrderItem.findById(el).remove().exec())
    }).catch(e => { res.status(400).json({ success: false, data: e }) });

    res.status(201).json({ success: true, data: null });
});

https://mongoosejs.com/docs/api/model.html#model_Model-remove

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNick ParsonsView Question on Stackoverflow
Solution 1 - MongodbJohnnyHKView Answer on Stackoverflow
Solution 2 - MongodbTalha AwanView Answer on Stackoverflow
Solution 3 - MongodbSam BelleroseView Answer on Stackoverflow
Solution 4 - MongodbMK.View Answer on Stackoverflow
Solution 5 - MongodbIgor ZView Answer on Stackoverflow