Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/rubysherpas/paranoia
acts_as_paranoid for Rails 5, 6 and 7
https://github.com/rubysherpas/paranoia
Last synced: 11 days ago
JSON representation
acts_as_paranoid for Rails 5, 6 and 7
- Host: GitHub
- URL: https://github.com/rubysherpas/paranoia
- Owner: rubysherpas
- License: other
- Created: 2010-09-12T08:57:53.000Z (about 14 years ago)
- Default Branch: core
- Last Pushed: 2024-05-27T11:07:45.000Z (6 months ago)
- Last Synced: 2024-06-16T05:28:56.938Z (5 months ago)
- Language: Ruby
- Homepage:
- Size: 435 KB
- Stars: 2,861
- Watchers: 33
- Forks: 524
- Open Issues: 100
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
- awesome-rails-gem - paranoia - ActiveRecord plugin allowing you to hide and restore records without actually deleting them. (Active Record / Omniauth)
README
[![Gem Version](https://badge.fury.io/rb/paranoia.svg)](https://badge.fury.io/rb/paranoia)
[![build](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml/badge.svg)](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml)**Notice:**
`paranoia` has some surprising behaviour (like overriding ActiveRecord's `delete` and `destroy`) and is not recommended for new projects. See [`discard`'s README](https://github.com/jhawthorn/discard#why-not-paranoia-or-acts_as_paranoid) for more details.
Paranoia will continue to accept bug fixes and support new versions of Rails but isn't accepting new features.
# Paranoia
Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/ActsAsParanoid/acts_as_paranoid) for Rails 3/4/5, using much, much, much less code.
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](#destroying-through-association-callbacks) for clarifying examples.***
## Getting Started Video
Setup and basic usage of the paranoia gem
[GoRails #41](https://gorails.com/episodes/soft-delete-with-paranoia)## Installation & Usage
For Rails 3, please use version 1 of Paranoia:
``` ruby
gem "paranoia", "~> 1.0"
```For Rails 4 and 5, please use version 2 of Paranoia (2.2 or greater required for rails 5):
``` ruby
gem "paranoia", "~> 2.2"
```Of course you can install this from GitHub as well from one of these examples:
``` ruby
gem "paranoia", github: "rubysherpas/paranoia", branch: "rails3"
gem "paranoia", github: "rubysherpas/paranoia", branch: "rails4"
gem "paranoia", github: "rubysherpas/paranoia", branch: "rails5"
```Then run:
``` shell
bundle install
```Updating is as simple as `bundle update paranoia`.
#### Run your migrations for the desired models
Run:
``` shell
bin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index
```and now you have a migration
``` ruby
class AddDeletedAtToClients < ActiveRecord::Migration
def change
add_column :clients, :deleted_at, :datetime
add_index :clients, :deleted_at
end
end
```### Usage
#### In your model:
``` ruby
class Client < ActiveRecord::Base
acts_as_paranoid# ...
end
```Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column:
``` ruby
>> client.deleted_at
# => nil
>> client.destroy
# => client
>> client.deleted_at
# => [current timestamp]
```If you really want it gone *gone*, call `really_destroy!`:
``` ruby
>> client.deleted_at
# => nil
>> client.really_destroy!
# => client
```If you need skip updating timestamps for deleting records, call `really_destroy!(update_destroy_attributes: false)`.
When we call `really_destroy!(update_destroy_attributes: false)` on the parent `client`, then each child `email` will also have `really_destroy!(update_destroy_attributes: false)` called.``` ruby
>> client.really_destroy!(update_destroy_attributes: false)
# => client
```If you want to use a column other than `deleted_at`, you can pass it as an option:
``` ruby
class Client < ActiveRecord::Base
acts_as_paranoid column: :destroyed_at...
end
```If you want to skip adding the default scope:
``` ruby
class Client < ActiveRecord::Base
acts_as_paranoid without_default_scope: true...
end
```If you want to access soft-deleted associations, override the getter method:
``` ruby
def product
Product.unscoped { super }
end
```If you want to include associated soft-deleted objects, you can (un)scope the association:
``` ruby
class Person < ActiveRecord::Base
belongs_to :group, -> { with_deleted }
endPerson.includes(:group).all
```If you want to find all records, even those which are deleted:
``` ruby
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):
``` ruby
Client.without_deleted
```If you want to find only the deleted records:
``` ruby
Client.only_deleted
```If you want to check if a record is soft-deleted:
``` ruby
client.paranoia_destroyed?
# or
client.deleted?
```If you want to restore a record:
``` ruby
Client.restore(id)
# or
client.restore
```If you want to restore a whole bunch of records:
``` ruby
Client.restore([id1, id2, ..., idN])
```If you want to restore a record and their dependently destroyed associated records:
``` ruby
Client.restore(id, :recursive => true)
# or
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:
``` ruby
Client.restore(id, :recursive => true, :recovery_window => 2.minutes)
# or
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:
``` ruby
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,``` ruby
add_index :clients, :group_id
add_index :clients, [:group_id, :other_id]
```should be replaced with
``` ruby
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.``` ruby
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.``` ruby
add_column :clients, :active, :boolean
add_index :clients, [:group_id, :active], unique: trueclass Client < ActiveRecord::Base
# optionally have paranoia make use of your unique column, so that
# your lookups will benefit from the unique index
acts_as_paranoid column: :active, sentinel_value: truedef paranoia_restore_attributes
{
deleted_at: nil,
active: true
}
enddef 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:
``` ruby
class Client < ActiveRecord::Base
acts_as_paranoidhas_many :emails, dependent: :destroy
endclass Email < ActiveRecord::Base
acts_as_paranoidbelongs_to :client
end
```When we call `destroy` on the parent `client`, it will call `destroy` on all of its associated children `emails`:
``` ruby
>> client.emails.count
# => 5
>> client.destroy
# => client
>> client.deleted_at
# => [current timestamp]
>> Email.where(client_id: client.id).count
# => 0
>> Email.with_deleted.where(client_id: client.id).count
# => 5
```Similarly, when we call `really_destroy!` on the parent `client`, then each child `email` will also have `really_destroy!` called:
``` ruby
>> client.emails.count
# => 5
>> client.id
# => 12345
>> client.really_destroy!
# => client
>> Client.find 12345
# => ActiveRecord::RecordNotFound
>> Email.with_deleted.where(client_id: client.id).count
# => 0
```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:
``` ruby
class Client < ActiveRecord::Base
acts_as_paranoidhas_many :emails, dependent: :destroy
endclass Email < ActiveRecord::Base
belongs_to :client
end>> client.emails.count
# => 5
>> client.destroy
# => client
>> Email.where(client_id: client.id).count
# => 0
>> Email.with_deleted.where(client_id: client.id).count
# => NoMethodError: undefined method `with_deleted' for #
```## 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:
```ruby
class Product < ActiveRecord::Base
acts_as_paranoidafter_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.