Skip to content

A Ruby gem that adds a better option to recursively delete dependent ActiveRecords.

License

Notifications You must be signed in to change notification settings

jaynetics/delete_recursively

Repository files navigation

DeleteRecursively

Gem Version Build Status Coverage

This gem adds a new option for ActiveRecord associations:

dependent: :delete_recursively

When you destroy a record, all records that are associated using this option will be deleted recursively, without instantiating any of them. See below for a more detailed explanation of why this is cool.

Note that, just like dependent: :delete or dependent: :delete_all, this new option does not trigger the around/before/after_destroy callbacks of the dependent records.

However, it is possible to have dependent: :destroy associations anywhere within a chain of models that are otherwise associated with dependent: :delete_recursively. The :destroy option will work normally anywhere up or down the line, instantiating and destroying all relevant records and thus also triggering their callbacks.

Installation

Add, install, or require delete_recursively.

Usage

Assume we have these classes:

class Blog < ApplicationRecord
  has_many :posts, dependent: :delete_recursively
end

class Post < ApplicationRecord
  belongs_to :blog
  has_many :comments, dependent: :delete
end

class Comment < ApplicationRecord
  belongs_to :post
end

This will delete my_blog, all of it's posts, and all comments belonging to any of these posts:

my_blog.destroy

Note that using dependent: :delete ends the recursion. If the Comment model above had any further associations, these would not be touched.

Utility methods

There is also ActiveRecord::Base#delete_recursively to recursively delete a single record while skipping its own destroy callbacks, e.g.:

my_blog.delete_recursively
# use `force: true` to call `#delete` even for `destroy` associations
my_blog.delete_recursively(force: true)

ActiveRecord::Relation#delete_all_recursively can be used to delete a bunch of records recursively, e.g.:

Blog.where(user_id: evil_user_id).delete_all_recursively

There is also the utility command ::all for mass operations. This will delete all Blogs, Posts, and Comments (even orphans):

DeleteRecursively.all(Blog)

::all accepts a criteria Hash to limit the action's scope, much like ActiveRecord::delete_all. For any model in the chain that has the corresponding columns, these criteria will limit which records are deleted. For instance, assuming that all our models have timestamps and a user_id, this will delete all Blogs, Posts, and Comments created by evil_user in the last two days:

DeleteRecursively.all(Blog, created_at: 2.days.ago..Time.now, user_id: evil_user.id)

Explanation

Generally speaking, this gem addresses the dilemma that on the one hand a chain of associations with the dependent: :destroy option works recursively, but is very inefficient, whereas on the other hand a chain of associations with the dependent: :delete_all option is efficient, but works only to a depth of one level.

:delete_recursively works to any depth and is efficient.

Let's assume you have a Blog model. There is one Blog record that you want to destroy. This Blog record has 100 Post records. Each of these Post records has about 10 Comment records.

Now let's assume these models are chained together with dependent: :destroy.

In that case, destroying the Blog record will instantiate all Posts and Comments and various related objects, and destroy all of these records individually. That means instantiating 10.000s of objects and performing countless SQL calls.

With dependent: :delete_recursively, that will take just a tiny, fixed number of objects and a tiny, fixed number of SQL calls. The number of records no longer matters, because associated records are found by evaluating associations defined on the model class and finding and deleting dependent records in batches.

Credits

DeleteRecursively was heavily inspired by JD Isaacks' gem recurse-delete. recurse-delete works a litte differently, though. It adds a new method to ActiveRecord instances. This method can then be called manually on a record, and it will efficiently delete the record and all of it's dependencies that have the :delete, :delete_all or :destroy option.