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});

});

Solution 1

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

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

Here's an other way I found

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

Solution 4

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

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