60

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

});
Nick Parsons
  • 8,377
  • 13
  • 48
  • 70

6 Answers6

140

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.

JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • Sorry for the newbie question, but would this be nested inside of my Client.findById() call? Sorry, any code examples would be helpful. I've read the documentation, but want to make sure that I'm doing everything the correct way, the first time. Thanks again! – Nick Parsons Jan 16 '13 at 16:05
  • 1
    No, you'd call this on the `Schema` object used to define the `Client` model prior to creating the model via a `mongoose.model` or `db.model` call. – JohnnyHK Jan 16 '13 at 16:34
  • 2
    Worked perfectly when I moved it into my schema definition file (model). I had to add .exec() to the end of the remove calls though. No harm in that, right? – Nick Parsons Jan 16 '13 at 17:11
  • 1
    You're right, you need to do that with `remove` when you're not supplying a callback. I updated the answer. – JohnnyHK Jan 16 '13 at 17:38
  • 2
    @JohnnyHK: would this method be called only if we call client.remove(). In my app I delete it using the following statement: Client.findByIdAndRemove(client_id,function(err){}) Would this pre hook not get called? – inquisitive Apr 22 '15 at 11:35
  • 1
    @Inquisitive Right, the hook would not be called in that case. It's only called when you call `remove` on a doc instance. – JohnnyHK Apr 22 '15 at 12:46
  • @JohnnyHK yeah, i found that out following some SO posts.thnx. But now, I'm running into some other issue, though not much related to this post, just putting here in short. my app is crashing after returning from the hook and deleting the record, due to the `res.send` call. stating `can't set headers after they are sent`, – inquisitive Apr 22 '15 at 12:52
  • Don't forget the `.exec()` in `.remove().exec()`. It's `.exec()` or a callback to make it go. – Michael Cole Jul 10 '15 at 16:01
  • @JohnnyHK, thanks a lot. You made my day. Funny though, how such a simple solution didn't strike me. – havish Oct 27 '15 at 20:01
  • To get this to work, I couldn't call `.remove()` on the model, but rather an *instance* of the model. https://github.com/Automattic/mongoose/issues/1241 is where I got my solution, where the guy does `findOneAndRemove` and then calls `.remove()` on the object in the callback. – tscizzle Mar 02 '16 at 06:04
  • A better explanation of what my previous comment is talking about: http://stackoverflow.com/questions/29016354/mongoose-post-remove-event-doesnt-fire. – tscizzle Mar 02 '16 at 06:06
  • @tscizzle Right, this works because `client` is an instance of the model in the OP's question, so middleware can be used. A call to `remove()` on the model is effectively a pass-through to the native driver. – JohnnyHK Mar 02 '16 at 15:21
  • @JohnnyHK I was more talking about the Sweepstakes and Submission collections, because as it is, *those* calls will not trigger hooks, but I guess OP isn't wondering about hooks on those. That was just my own use case of deleting a "user," having all "conversations" of that user be deleted, and then have all "messages" of those conversations be deleted (so there was an extra cascade step). – tscizzle Mar 02 '16 at 15:38
  • Is it guarantied that whatever goes inside a middleware will run atomically, as part of the same transaction? – fmagno Feb 18 '21 at 13:47
  • @fmagno No, middleware runs independently. – JohnnyHK Feb 18 '21 at 14:27
  • @JohnnyHK, how would you go about implementing a solution for this related question: https://stackoverflow.com/questions/66276676/mongoose-middleware-rollback-operations-performed-by-pre-post-hooks-when-an – fmagno Feb 19 '21 at 12:09
  • Can we have a library that handles this? – olawalejuwonm Aug 07 '23 at 12:53
12

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

Talha Awan
  • 4,573
  • 4
  • 25
  • 40
  • Please could you put a example using OneToMany? I'm not right if this code remove all children and update the reference and the parent. – Francis Rodrigues Feb 14 '18 at 14:55
  • Not sure I understood your comment. But let me explain my answer with example. Say there are 3 client docs: C1, C2, C3. And there are number of submission docs, one of them is S273. Now id of S273 exists in `submission_ids` of C1 and C3 (OneToMany, i.e. Client has many submissions). My answer above addresses the scenario where you want to remove S273. When you do, the id of S273 will be removed from `submission_ids` of both C1 and C3 automatically. – Talha Awan Feb 14 '18 at 19:30
  • Well.. I understood that `submissionSchema` is a child collection referenced on `Client schema` right? – Francis Rodrigues Feb 14 '18 at 22:12
  • Yes. This is "hasMany" way. The client holds array of many submission ids. The other is "belongsTo" way, in which each submission document has one client's id which it belongs to. – Talha Awan Feb 15 '18 at 12:11
  • It's perfect! <3 Why I not founded it in Mongoose doc? o.o – Francis Rodrigues Feb 15 '18 at 12:41
  • 1
    @FrancisRodrigues because it's related to NoSql not Mongoose (which is just a node library/wrapper for mongodb to help ease development). I'm not sure mongodb has a section to explain this but couchbase (another NoSql db) has a super informative docs explaining it in detail. https://developer.couchbase.com/documentation/server/4.6/data-modeling/modeling-relationships.html, https://developer.couchbase.com/documentation/server/3.x/developer/dev-guide-3.0/doc-design-considerations.html, https://developer.couchbase.com/documentation/server/3.x/developer/dev-guide-3.0/model-docs-retrieval.html – Talha Awan Feb 16 '18 at 12:06
  • Thank you so much, Talha Awan! – Francis Rodrigues Feb 16 '18 at 12:09
3

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.

MK.
  • 752
  • 1
  • 8
  • 12
  • userSchema.post('findOneAndDelete', async id => { await Comments.deleteMany({ user: id }); }); findOneAndDelete method also works here it seems remove method is deprecated – Mohammed Feb 03 '22 at 11:07
2

Here's an other way I found

submissionSchema.pre('remove', function(next) {
    this.model('Client').remove({ submission_ids: this._id }, next);
    next();
});
Sam Bellerose
  • 1,782
  • 2
  • 18
  • 43
0

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

Igor Z
  • 601
  • 6
  • 7
  • 1
    Jesus, This is a trick not `onDelete`. So I guess you meant that mongoose/MongoDB does not support onDelete, right? – Kasir Barati Aug 13 '22 at 13:19
0

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.