Paranoia
When your app is using Paranoia, calling destroy
on an ActiveRecord object doesn't actually destroy the database record, but just hides it. Paranoia does this by setting a deleted_at
field to the current time when you destroy
a record, and hides it by scoping all queries on your model to only include records which do not have a deleted_at
field.
If you wish to actually destroy an object you may call really_destroy!
. WARNING: This will also really destroy all dependent: :destroy
records, so please aim this method away from face when using.
If a record has has_many
associations defined AND those associations have dependent: :destroy
set on them, then they will also be soft-deleted if acts_as_paranoid
is set, otherwise the normal destroy will be called. See Destroying through association callbacks for clarifying examples.
Installation & Usage
gem "kumolus-paranoia"
Then run:
bundle install
Run your migrations for the desired models
Run:
bin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index
and now you have a migration
class AddDeletedAtToClients < ActiveRecord::Migration
def change
add_column :clients, :deleted_at, :datetime
add_index :clients, :deleted_at
end
end
Usage
In your model:
class Client < ActiveRecord::Base
acts_as_paranoid
end
Hey presto, it's there! Calling destroy
will now set the deleted_at
column:
>> client.deleted_at
>> client.destroy
>> client.deleted_at
If you really want it gone gone, call really_destroy!
:
>> client.deleted_at
>> client.really_destroy!
If you want to use a column other than deleted_at
, you can pass it as an option:
class Client < ActiveRecord::Base
acts_as_paranoid column: :destroyed_at
...
end
If you want to skip adding the default scope:
class Client < ActiveRecord::Base
acts_as_paranoid without_default_scope: true
...
end
If you want to access soft-deleted associations, override the getter method:
def product
Product.unscoped { super }
end
If you want to include associated soft-deleted objects, you can (un)scope the association:
class Person < ActiveRecord::Base
belongs_to :group, -> { with_deleted }
end
Person.includes(:group).all
If you want to find all records, even those which are deleted:
Client.with_deleted
If you want to exclude deleted records, when not able to use the default_scope (e.g. when using without_default_scope):
Client.without_deleted
If you want to find only the deleted records:
Client.only_deleted
If you want to check if a record is soft-deleted:
client.paranoia_destroyed?
client.is_deleted?
If you want to restore a record:
Client.restore(id)
client.restore
If you want to restore a whole bunch of records:
Client.restore([id1, id2, ..., idN])
If you want to restore a record and their dependently destroyed associated records:
Client.restore(id, :recursive => true)
client.restore(:recursive => true)
If you want to restore a record and only those dependently destroyed associated records that were deleted within 2 minutes of the object upon which they depend:
Client.restore(id, :recursive => true. :recovery_window => 2.minutes)
client.restore(:recursive => true, :recovery_window => 2.minutes)
Note that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model.
A Rails validator is provided should you require this functionality:
validates :some_assocation, association_not_soft_destroyed: true
This validator makes sure that some_assocation
is not soft destroyed. If the object is soft destroyed the main object is rendered invalid and an validation error is added.
For more information, please look at the tests.
About indexes:
Beware that you should adapt all your indexes for them to work as fast as previously.
For example,
add_index :clients, :group_id
add_index :clients, [:group_id, :other_id]
should be replaced with
add_index :clients, :group_id, where: "deleted_at IS NULL"
add_index :clients, [:group_id, :other_id], where: "deleted_at IS NULL"
Of course, this is not necessary for the indexes you always use in association with with_deleted
or only_deleted
.
Unique Indexes
Because NULL != NULL in standard SQL, we can not simply create a unique index
on the deleted_at column and expect it to enforce that there only be one record
with a certain combination of values.
If your database supports them, good alternatives include partial indexes
(above) and indexes on computed columns. E.g.
add_index :clients, [:group_id, 'COALESCE(deleted_at, false)'], unique: true
If not, an alternative is to create a separate column which is maintained
alongside deleted_at for the sake of enforcing uniqueness. To that end,
paranoia makes use of two method to make its destroy and restore actions:
paranoia_restore_attributes and paranoia_destroy_attributes.
add_column :clients, :active, :boolean
add_index :clients, [:group_id, :active], unique: true
class Client < ActiveRecord::Base
acts_as_paranoid column: :active, sentinel_value: true
def paranoia_restore_attributes
{
deleted_at: nil,
active: true
}
end
def paranoia_destroy_attributes
{
deleted_at: current_time_from_proper_timezone,
active: nil
}
end
end
Destroying through association callbacks
When dealing with dependent: :destroy
associations and acts_as_paranoid
, it's important to remember that whatever method is called on the parent model will be called on the child model. For example, given both models of an association have acts_as_paranoid
defined:
class Client < ActiveRecord::Base
acts_as_paranoid
has_many :emails, dependent: :destroy
end
class Email < ActiveRecord::Base
acts_as_paranoid
belongs_to :client
end
When we call destroy
on the parent client
, it will call destroy
on all of its associated children emails
:
>> client.emails.count
>> client.destroy
>> client.deleted_at
>> Email.where(client_id: client.id).count
>> Email.with_deleted.where(client_id: client.id).count
Similarly, when we call really_destroy!
on the parent client
, then each child email
will also have really_destroy!
called:
>> client.emails.count
>> client.id
>> client.really_destroy!
>> Client.find 12345
>> Email.with_deleted.where(client_id: client.id).count
However, if the child model Email
does not have acts_as_paranoid
set, then calling destroy
on the parent client
will also call destroy
on each child email
, thereby actually destroying them:
class Client < ActiveRecord::Base
acts_as_paranoid
has_many :emails, dependent: :destroy
end
class Email < ActiveRecord::Base
belongs_to :client
end
>> client.emails.count
>> client.destroy
>> Email.where(client_id: client.id).count
>> Email.with_deleted.where(client_id: client.id).count
Acts As Paranoid Migration
You can replace the older acts_as_paranoid
methods as follows:
Old Syntax | New Syntax |
---|
find_with_deleted(:all) | Client.with_deleted |
find_with_deleted(:first) | Client.with_deleted.first |
find_with_deleted(id) | Client.with_deleted.find(id) |
The recover
method in acts_as_paranoid
runs update
callbacks. Paranoia's
restore
method does not do this.
Callbacks
Paranoia provides several callbacks. It triggers destroy
callback when the record is marked as deleted and real_destroy
when the record is completely removed from database. It also calls restore
callback when the record is restored via paranoia
For example if you want to index your records in some search engine you can go like this:
class Product < ActiveRecord::Base
acts_as_paranoid
after_destroy :update_document_in_search_engine
after_restore :update_document_in_search_engine
after_real_destroy :remove_document_from_search_engine
end
You can use these events just like regular Rails callbacks with before, after and around hooks.
License
This gem is released under the MIT license.