DEV Community

Kwabena Bio Berko
Kwabena Bio Berko

Posted on

Implementing SQL- Like Cascades in Mongoose

I was very recently working on a hobby project: a role based access control application where I needed to implement cascade delete in some of the models.
The user model for instance, had a roles attribute which was an array of the ObjectId's of the role model:

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  username: {
    type: String,
    unique: true,
    required: true
  },
  password: {
    type: String,
    required: true
  },
  roles: [
    {
      type: mongoose.Schema.Types.ObjectId,
      required: true,
      ref: "Role"
    }
  ]
});

Enter fullscreen mode Exit fullscreen mode

Roles are dynamically created and assigned to users. The problem I was facing though, is that when a Role is deleted, it's ObjectId references in the user model were not also deleted, therefore becoming orphaned, hence breaking referential integrity.

To prevent this, I needed to implement cascade delete. In SQL, it's as simple as adding the ON DELETE CASCADE referential action when creating your users_roles table like so:

CREATE TABLE users_roles (
    user_id int unsigned not null, 
    role_id int unsigned not null, 
    PRIMARY KEY (user_id, role_id), 
    FOREIGN KEY (user_id) REFERENCES users (id) 
        ON DELETE CASCADE, 
    FOREIGN KEY(role_id) REFERENCES roles (id)
            ON DELETE CASCADE
);
Enter fullscreen mode Exit fullscreen mode

But I was using mongodb and the mongoose ODM. How was I supposed to go about this? I was able to solve this by utilizing mongoose middlewares. As per their documentation, mongoose middlewares or hooks are functions that are passed control during execution of asynchronous functions. The post middleware or hook in particular is exactly what I needed as it would allow me to perform some actions immediately after a database operation completes:

const mongoose = require("mongoose");
const User = require("./user");

const RoleSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true
  }
});

RoleSchema.post("remove", document => {
  const roleId = document._id;
  User.find({ roles: { $in: [roleId] } }).then(users => {
    Promise.all(
      users.map(user =>
        User.findOneAndUpdate(
          user._id,
          { $pull: { roles: roleId } },
          { new: true }
        )
      )
    );
  });
});

module.exports = mongoose.model("Role", RoleSchema);

Enter fullscreen mode Exit fullscreen mode

Let me briefly describe the code above. Immediately after deleting a particular role, I find all users that have that role id in their array of roles using mongodb's $in operator (mongoose allows you to use native mongodb operators in your queries).

....
User.find({ roles: { $in: [roleId] } })
Enter fullscreen mode Exit fullscreen mode

Then for each user that satisfies that criteria, another operator, the $pull operator is used to "pull" or remove that specific role id from their array of roles.

....
User.findOneAndUpdate(
  user._id,
  { $pull: { roles: roleId } },
  { new: true }
)
Enter fullscreen mode Exit fullscreen mode

This is the approach I used to implement cascade delete in mongoose to ensure referential integrity. I would love to hear of the other approaches you might use to solve a similar problem in mongoose.

Happy coding, everyone!

Top comments (5)

Collapse
 
dmfay profile image
Dian Fay

The catch here is that you're hitting the database as many times as you have matching users, plus the query to find the matching users in the first place. It doesn't hurt much at smaller scales but it'll quickly grind things to a halt once you have more data.

I know Mongo has update with multi or updateMany which could improve matters, although I don't know if Mongoose has different idioms. But honestly, my first recommendation here is to reevaluate Mongo. The portion of your data model shown here is purely relational, and you're having to crudely approximate relational database features to do what you want. Why not just use a relational database?

Collapse
 
kwabenberko profile image
Kwabena Bio Berko • Edited

Thanks for the comment, Dian. I really appreciate it. Yes, normally I would use a relational database for this kind of application due to the relationships between the models. SQL gives you cascades and much more right out of the box.I just wanted to experiment on how cascades could be achieved using mongoose :)

Collapse
 
phildebbs profile image
Philip Debrah

Great!

Collapse
 
dre4success profile image
Damilare Joseph

Nice article. Learnt a new thing today. Will implement this in my code and see how it goes

Collapse
 
kwabenberko profile image
Kwabena Bio Berko

Thanks!