Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mongoose: deleteOne middleware for cascading delete not working

With remove being deprecated in Mongoose 5.7.13, I want to use deleteOne instead. I need to get the id of the deleted document so that I can then delete further related documents in other collections in a cascade. I thought that "this" within the context of the pre middleware hook was meant to refer to the removed document, but instead it's just an empty object. Is there a canonical working example of this? I'm still currently using 5.7.12 at this point - will that make a difference here?

Here is the code I'm currently working with. The issue is that I can't get the projectId at the start because the reference is completely empty. Doing this on post rather than pre, or switching the option to run on query rather than document all yield the same result.

ProjectSchema.pre("deleteOne", {document:true}, (next) => {
  const projectId = this._id;
  ListModel.find({parentProject:projectId}, (err, lists) => {
    if(err){
      console.log("error cascading project delete to lists", {err});
    }
    lists.map(list => {
      ListModel.deleteOne({_id:list._id}, (err, result) => {
        if(err) {
          console.log("error on project delete cascade", {err});
        }
      });
    });
  });
});
like image 323
notnot Avatar asked Dec 02 '19 22:12

notnot


People also ask

How do you delete an item from Mongodb using mongoose?

remove() is deprecated and you can use deleteOne(), deleteMany(), or bulkWrite() instead. As of "mongoose": ">=2.7. 1" you can remove the document directly with the . remove() method rather than finding the document and then removing it which seems to me more efficient and easy to maintain.

What is .pre in mongoose?

Sponsor #native_company# — #native_desc# Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins.

What does Mongoose deleteOne return?

The deleteOne() method returns an object containing three fields. So in our result, n is 1, and it should be 1 because the deleteOne() method only matches a single document. The deletedCount is also 1, this means 1 document was deleted successfully.

What is the mongoose command used to delete an item?

There are a few different ways to perform delete operations in Mongoose: deleteOne() to delete a single document, deleteMany() to delete multiple documents and remove() to delete one or multiple documents.


2 Answers

It depends whether you call deleteOne on document or on model. The later just have no document to bind it to.

The former gives you the document as you expect:

const project = await ProjectModel.findOne();
project.deleteOne();

The later gives you the Query. There is no _id in the query, but it has this.op for example, which in this middleware will be "deleteOne":

await ProjectModel.deleteOne();

The only way to get the document id in this case is to ensure it is provided in the query:

await ProjectModel.deleteOne({_id: "alex"});

Then you can get it in the middleware from the filter:

const projectId = this.getFilter()["_id"]

You can specify query: false in second parameter of the middleware to ensure the it is not invoked when you call deleteOne on model. So the best you can do:

ProjectSchema.pre("deleteOne", {document:true, query: false}, (next) => {
    const projectId = this._id;
    ....
});

ProjectSchema.pre("deleteOne", {document:false, query: true}, (next) => {
    const projectId = this.getFilter()["_id"];
    if (typeof projectId === "undefined") {
        // no way to make cascade deletion since there is no _id
        // in the delete query
        // I would throw an exception, but it's up to you how to deal with it
        // to ensure data integrity
    }
});

Please take a look at corresponding tests on v5.7.12: https://github.com/Automattic/mongoose/blob/5.7.12/test/model.middleware.test.js#L436

like image 76
Alex Blex Avatar answered Sep 28 '22 11:09

Alex Blex


In the mongoose docs it says "Model.deleteOne() does not trigger pre('remove') or post('remove') hooks."

There is solution if you can refactor your delete operations with findByIdAndDelete, it triggers the findOneAndDelete middleware,

So we can add this middleware to Project Schema.

Project model:

const mongoose = require("mongoose");
const ProjectChild = require("./projectChild");

const ProjectSchema = new mongoose.Schema({
  name: String
});

ProjectSchema.post("findOneAndDelete", async function(doc) {
  console.log(doc);

  if (doc) {
    const deleteResult = await ProjectChild.deleteMany({
      parentProject: doc._id
    });

    console.log("Child delete result: ", deleteResult);
  }
});

module.exports = mongoose.model("Project", ProjectSchema);

ProjectChild model:

const mongoose = require("mongoose");

const projectChildSchema = new mongoose.Schema({
  name: String,
  parentProject: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "Project"
  }
});

module.exports = mongoose.model("ProjectChild", projectChildSchema);

I created a project like this:

{
    "_id": "5dea699cb10c442260245abf",
    "name": "Project 1",
    "__v": 0
}

And created 2 project child for this project:

Child 1

{
    "_id": "5dea69c7b10c442260245ac0",
    "name": "Child 1 (project 1)",
    "parentProject": "5dea699cb10c442260245abf",
    "__v": 0
}

Child 2

{
    "_id": "5dea69e8b10c442260245ac1",
    "name": "Child 2 (project 1)",
    "parentProject": "5dea699cb10c442260245abf",
    "__v": 0
}

I created a sample route to delete a project by its id like this:

router.delete("/project/:id", async (req, res) => {
  const result = await Project.findByIdAndDelete(req.params.id);

  res.send(result);
});

When I send a DELETE request to this route, we see the following info in the console:

console.log(doc);

{ _id: 5dea699cb10c442260245abf, name: 'Project 1', __v: 0 }

console.log("Child delete result: ", deleteResult);

Child delete result:  { n: 2, ok: 1, deletedCount: 2 }

So we could deleted the 2 children of the project, when we deleted the project.

As an alternative you can also use findOneAndRemove, it triggers findOneAndRemove post middleware.

So in the ProjectSchema we replace the post middleware like this:

ProjectSchema.post("findOneAndRemove", async function(doc) {
  console.log(doc);

  if (doc) {
    const deleteResult = await ProjectChild.deleteMany({
      parentProject: doc._id
    });

    console.log("Child delete result: ", deleteResult);
  }
});

When we use a findOneAndRemove operation, the result will be the same as the first alternative:

const result = await Project.findOneAndRemove({ _id: req.params.id });
like image 45
SuleymanSah Avatar answered Sep 28 '22 10:09

SuleymanSah