Nick Parsons
Nick Parsons

Reputation: 8607

Cascade style delete in Mongoose

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

});

Upvotes: 62

Views: 58298

Answers (6)

Abhishek Upadhyay
Abhishek Upadhyay

Reputation: 37

As with the Mongoose core, related documents are specified with a combination of type:mongoose.Schema.Types.ObjectId and ref:'Related_Model'. This plugin adds two more configuration options to ObjectID types: $through and $cascadeDelete.

$through defines the path on the related document that is a reference back to this document. If you have two schema like so:

var cascadingRelations = require('cascading-relations'); var fooSchema = new mongoose.Schema({ title:String, bars:[{ type:mongoose.Schema.Types.ObjectId, ref:'Bar', $through:'foo' }] });

// Apply the plugin fooSchema.plugin(cascadingRelations);

var barSchema = new mongoose.Schema({ title:String, foo:{ type:mongoose.Schema.Types.ObjectId, ref:'Foo' } });

// Apply the plugin barSchema.plugin(cascadingRelations);

if you query the database immediately after running remove(), the cascade delete processes still may not have finished. In our tests, we get around this by simply waiting 5 seconds before checking if the process was successful.

Upvotes: 0

Igor Z
Igor Z

Reputation: 631

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

Upvotes: 0

MK.
MK.

Reputation: 832

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.

Upvotes: 4

JohnnyHK
JohnnyHK

Reputation: 311855

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.

Upvotes: 142

Sam Bellerose
Sam Bellerose

Reputation: 1812

Here's an other way I found

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

Upvotes: 2

Talha Awan
Talha Awan

Reputation: 4619

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().

Upvotes: 12

Related Questions