base on acts_as_paranoid for Rails 5, 6 and 7 [![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 } end Person.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) ``` If you want to trigger an after_commit callback when restoring a record: ``` ruby class Client < ActiveRecord::Base acts_as_paranoid after_restore_commit: true after_commit :commit_called, on: :restore # or after_restore_commit :commit_called ... end ``` 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: true class 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: 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: ``` ruby 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`: ``` 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_paranoid has_many :emails, dependent: :destroy end class 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 #<Class:0x0123456> ``` #### delete_all: The gem supports `delete_all` method, however it is disabled by default, to enable it add this in your `environment` file ``` ruby Paranoia.delete_all_enabled = true ``` alternatively, you can enable/disable it for specific models as follow: ``` ruby class User < ActiveRecord::Base acts_as_paranoid(delete_all_enabled: true) end ``` ## 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_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. ", Assign "at most 3 tags" to the expected json: {"id":"11455","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"