/cascade-deleter

A ruby gem designed to delete items and all of their descending hierarchy items.

Primary LanguageRubyMIT LicenseMIT

Cascade Delete

Cascade Deleter is a ruby gem designed to delete a set of items with all of their children, grandchildren, grandgrandchildren, i.e. delete items and all of their descending hierarchy.

Why it is necessary? ๐Ÿ’ก

Currently, Rails doesn't have a builtin one-liner way to delete items with all of their descending hierarchy, this type of deletion requires manual and tedious work, since you need to discover which items should be deleted for each descending classes, one by one.

Well, CascadeDeleter solves this issue perfectly with a oneliner command ๐Ÿ†

CascadeDeleter.new(MyModel.where(my_query)).delete_all

Example ๐Ÿง‘โ€๐Ÿซ

As an illustrative example, let's think about the following classes structure:

  1. Class Person has_many books
  2. Class Book has_many pages
  3. Class Page has_many words
  4. Class Word

That means: Person is parent of a list of Books, which is parent of a list of Pages, which is parent of a list of Words.

Now, imagine that you want to delete people with id = 1, id = 2 and id = 3 of the following hierarchy:

Hierarchy

Hierarchy

The correct solution would be to to delete it from the leaves to the root, which means deleting the items on this order:

Deletions Order

โ‘  โ†’ โ‘ก โ†’ โ‘ข โ†’ ๐Ÿšฉ

Deletion Order

โคท That means...

โ‘ . Delete the words that belongs to these people through the word โ†’ page โ†’ book โ†’ people relationship.

(Word A, Word B, Word C, Word D, Word E, Word F, Word G, Word H, Word I)

โ‘ก. Delete the pages that belongs to these people through the page โ†’ book โ†’ people relationship.

(Page A, Page B, Page C, Page D, Page E, Page F, Page G)

โ‘ข. Delete the books that belongs to these people through the book โ†’ people relationship.

(Book 1, Book 2, Book 3, Book 4)

๐Ÿšฉ. Finally deleting the people

(Person 1, Person 2, Person 3)

With the cascade-deleter gem, these deletions will be done automatically just executing the following oneliner command ๐Ÿ†

CascadeDeleter.new(Person.where(id: [1, 2, 3]).delete_all
# "Person.where(id: [1, 2, 3])" is used for this example, but you can place any ActiveRecord Relation as an argument here!

Installation โš™๏ธ

Add cascade-deleter to your Gemfile.

gem 'cascade-deleter'

Usage ๐Ÿš€

Just require the cascade_deleter library and use it! (You can test this on rails console)

Usage โ‘ 

Hard Delete of inactive Projects

CascadeDeleter.new(Project.unscoped.where(active: false)).delete_all

Usage โ‘ก

Hard Delete of inactive Projects by skipping some classes

CascadeDeleter.new(Project.unscoped.where(active: false)).delete_all(
  except: ['Audited::Audit', 'Picture', 'Attachment']
)

Usage โ‘ข

Hard Delete of inactive Projects overriding the joins parameter.

You can override the joins parameter through the custom_joins attribute if you want more accurate relationships in case the joins is not provided (Usage โ‘ ), the shortest path between each children class and the root class will be chosen for each join

CascadeDeleter.new(Project.unscoped.where(active: false)).delete_all(
  custom_joins: {
    'Attachment' => {:subproject=>:project}
  }
)

โคท That means: When deleting the Attachment descending class of Project, the following statement will be executed:

Attachment.joins({:subproject=>:project}).where(projects: { active: false }).delete_all

Usage โ‘ฃ

Soft Delete of TO BE DELETED Disciplines

CascadeDeleter.new(Discipline.where(description: '[TO BE DELETED]')).delete_all(
  method: :soft
)

โš ๏ธ When using Soft deletion (method: :soft), be aware that the active boolean parameter of your database tables will be used, so you need to have the active boolean parameter in your database tables when using Soft deletion.

t.boolean "active", default: true

Why not use dependent: :delete / dependent: :delete_all instead of CascadeDeleter? ๐Ÿค”

  1. ๐’๐ข๐ฆ๐ฉ๐ฅ๐ข๐œ๐ข๐ญ๐ฒ

The "dependent" solution not only will require you to add dependent: :delete / dependent: :delete_all on all the models you want to perform the cascade deletion, but it will still raise the Mysql2::Error: Cannot delete or update a parent row MySQL error while deleting the root items in case you don't have all of your database foreign keys setted up with foreign_key: { on_delete: :cascade }.

So, for example, if you want to delete 10 Projects that has 50 descending application models, you would need to add dependent: :delete / dependent: :delete_all on the 50 application models as well as executing new migrations changing all of the foreign keys of each one of these 50 tables to foreign_key: { on_delete: :cascade }.

In a comparison, if you decide to use CascadeDeleter, you would just need to execute this one-liner command which achieves the same goal:

CascadeDeleter.where(Project.where(id: (1..10))).delete_all
  1. ๐…๐ฅ๐ž๐ฑ๐ข๐›๐ข๐ฅ๐ข๐ญ๐ฒ

Another advantage is that you can perform Soft Deletions instead of Hard Deletions on your data, which can be very in handy for systems where you want to deactivate items instead of removing them completely from the database.

CascadeDeleter.where(Project.where(id: (1..10))).delete_all(method: :soft)
  1. ๐๐ž๐ซ๐Ÿ๐จ๐ซ๐ฆ๐š๐ง๐œ๐ž

Finally, you can also decide to delete a set of root items instead of deleting these root items one-by-one. This can be shown on the above examples, which deletes 10 Projects instead of deleting the projects individually. This fact increases the performance, since a single SQL delete operation is executed for a desired table. This process is also applied on the descending classes.

Contact


*This repository is maintained and developed by Victor Cordeiro Costa. For inquiries, partnerships, or support, don't hesitate to get in touch.