{"id":13400379,"url":"https://github.com/rubysherpas/paranoia","last_synced_at":"2025-12-16T21:26:55.266Z","repository":{"id":40990851,"uuid":"904810","full_name":"rubysherpas/paranoia","owner":"rubysherpas","description":"acts_as_paranoid for Rails 5, 6 and 7","archived":false,"fork":false,"pushed_at":"2025-03-16T14:26:11.000Z","size":475,"stargazers_count":2906,"open_issues_count":105,"forks_count":530,"subscribers_count":31,"default_branch":"core","last_synced_at":"2025-05-01T07:37:46.359Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rubysherpas.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2010-09-12T08:57:53.000Z","updated_at":"2025-04-26T15:36:31.000Z","dependencies_parsed_at":"2024-03-12T12:27:27.498Z","dependency_job_id":"c5d50554-d41b-4692-9556-06e584fabc17","html_url":"https://github.com/rubysherpas/paranoia","commit_stats":{"total_commits":406,"total_committers":153,"mean_commits":2.65359477124183,"dds":0.8596059113300493,"last_synced_commit":"7b96793a72f111eef474ca20fd88dd3d536d6633"},"previous_names":["radar/paranoia"],"tags_count":46,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubysherpas%2Fparanoia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubysherpas%2Fparanoia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubysherpas%2Fparanoia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubysherpas%2Fparanoia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rubysherpas","download_url":"https://codeload.github.com/rubysherpas/paranoia/tar.gz/refs/heads/core","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252154871,"owners_count":21702989,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T19:00:51.382Z","updated_at":"2025-12-16T21:26:50.205Z","avatar_url":"https://github.com/rubysherpas.png","language":"Ruby","readme":"[![Gem Version](https://badge.fury.io/rb/paranoia.svg)](https://badge.fury.io/rb/paranoia)\n[![build](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml/badge.svg)](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml)\n\n**Notice:**\n\n`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.\n\nParanoia will continue to accept bug fixes and support new versions of Rails but isn't accepting new features.\n\n# Paranoia\n\nParanoia 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.\n\nWhen 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.\n\nIf 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.\n\nIf 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.***\n\n## Getting Started Video\nSetup and basic usage of the paranoia gem\n[GoRails #41](https://gorails.com/episodes/soft-delete-with-paranoia)\n\n## Installation \u0026 Usage\n\nFor Rails 3, please use version 1 of Paranoia:\n\n``` ruby\ngem \"paranoia\", \"~\u003e 1.0\"\n```\n\nFor Rails 4 and 5, please use version 2 of Paranoia (2.2 or greater required for rails 5):\n\n``` ruby\ngem \"paranoia\", \"~\u003e 2.2\"\n```\n\nOf course you can install this from GitHub as well from one of these examples:\n\n``` ruby\ngem \"paranoia\", github: \"rubysherpas/paranoia\", branch: \"rails3\"\ngem \"paranoia\", github: \"rubysherpas/paranoia\", branch: \"rails4\"\ngem \"paranoia\", github: \"rubysherpas/paranoia\", branch: \"rails5\"\n```\n\nThen run:\n\n``` shell\nbundle install\n```\n\nUpdating is as simple as `bundle update paranoia`.\n\n#### Run your migrations for the desired models\n\nRun:\n\n``` shell\nbin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index\n```\n\nand now you have a migration\n\n``` ruby\nclass AddDeletedAtToClients \u003c ActiveRecord::Migration\n  def change\n    add_column :clients, :deleted_at, :datetime\n    add_index :clients, :deleted_at\n  end\nend\n```\n\n### Usage\n\n#### In your model:\n\n``` ruby\nclass Client \u003c ActiveRecord::Base\n  acts_as_paranoid\n\n  # ...\nend\n```\n\nHey presto, it's there! Calling `destroy` will now set the `deleted_at` column:\n\n\n``` ruby\n\u003e\u003e client.deleted_at\n# =\u003e nil\n\u003e\u003e client.destroy\n# =\u003e client\n\u003e\u003e client.deleted_at\n# =\u003e [current timestamp]\n```\n\nIf you really want it gone *gone*, call `really_destroy!`:\n\n``` ruby\n\u003e\u003e client.deleted_at\n# =\u003e nil\n\u003e\u003e client.really_destroy!\n# =\u003e client\n```\n\nIf you need skip updating timestamps for deleting records, call `really_destroy!(update_destroy_attributes: false)`.\nWhen 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.\n\n``` ruby\n\u003e\u003e client.really_destroy!(update_destroy_attributes: false)\n# =\u003e client\n```\n\nIf you want to use a column other than `deleted_at`, you can pass it as an option:\n\n``` ruby\nclass Client \u003c ActiveRecord::Base\n  acts_as_paranoid column: :destroyed_at\n\n  ...\nend\n```\n\n\nIf you want to skip adding the default scope:\n\n``` ruby\nclass Client \u003c ActiveRecord::Base\n  acts_as_paranoid without_default_scope: true\n\n  ...\nend\n```\n\nIf you want to access soft-deleted associations, override the getter method:\n\n``` ruby\ndef product\n  Product.unscoped { super }\nend\n```\n\nIf you want to include associated soft-deleted objects, you can (un)scope the association:\n\n``` ruby\nclass Person \u003c ActiveRecord::Base\n  belongs_to :group, -\u003e { with_deleted }\nend\n\nPerson.includes(:group).all\n```\n\nIf you want to find all records, even those which are deleted:\n\n``` ruby\nClient.with_deleted\n```\n\nIf you want to exclude deleted records, when not able to use the default_scope (e.g. when using without_default_scope):\n\n``` ruby\nClient.without_deleted\n```\n\nIf you want to find only the deleted records:\n\n``` ruby\nClient.only_deleted\n```\n\nIf you want to check if a record is soft-deleted:\n\n``` ruby\nclient.paranoia_destroyed?\n# or\nclient.deleted?\n```\n\nIf you want to restore a record:\n\n``` ruby\nClient.restore(id)\n# or\nclient.restore\n```\n\nIf you want to restore a whole bunch of records:\n\n``` ruby\nClient.restore([id1, id2, ..., idN])\n```\n\nIf you want to restore a record and their dependently destroyed associated records:\n\n``` ruby\nClient.restore(id, :recursive =\u003e true)\n# or\nclient.restore(:recursive =\u003e true)\n```\n\nIf 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:\n\n``` ruby\nClient.restore(id, :recursive =\u003e true, :recovery_window =\u003e 2.minutes)\n# or\nclient.restore(:recursive =\u003e true, :recovery_window =\u003e 2.minutes)\n```\n\nIf you want to trigger an after_commit callback when restoring a record:\n\n``` ruby\nclass Client \u003c ActiveRecord::Base\n  acts_as_paranoid after_restore_commit: true\n\n  after_commit          :commit_called, on: :restore\n  # or\n  after_restore_commit  :commit_called\n  ...\nend\n```\n\nNote that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model.\nA Rails validator is provided should you require this functionality:\n  ``` ruby\nvalidates :some_assocation, association_not_soft_destroyed: true\n```\nThis 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.\n\nFor more information, please look at the tests.\n\n#### About indexes:\n\nBeware that you should adapt all your indexes for them to work as fast as previously.\nFor example,\n\n``` ruby\nadd_index :clients, :group_id\nadd_index :clients, [:group_id, :other_id]\n```\n\nshould be replaced with\n\n``` ruby\nadd_index :clients, :group_id, where: \"deleted_at IS NULL\"\nadd_index :clients, [:group_id, :other_id], where: \"deleted_at IS NULL\"\n```\n\nOf course, this is not necessary for the indexes you always use in association with `with_deleted` or `only_deleted`.\n\n##### Unique Indexes\n\nBecause NULL != NULL in standard SQL, we can not simply create a unique index\non the deleted_at column and expect it to enforce that there only be one record\nwith a certain combination of values.\n\nIf your database supports them, good alternatives include partial indexes\n(above) and indexes on computed columns. E.g.\n\n``` ruby\nadd_index :clients, [:group_id, 'COALESCE(deleted_at, false)'], unique: true\n```\n\nIf not, an alternative is to create a separate column which is maintained\nalongside deleted_at for the sake of enforcing uniqueness. To that end,\nparanoia makes use of two method to make its destroy and restore actions:\nparanoia_restore_attributes and paranoia_destroy_attributes.\n\n``` ruby\nadd_column :clients, :active, :boolean\nadd_index :clients, [:group_id, :active], unique: true\n\nclass Client \u003c ActiveRecord::Base\n  # optionally have paranoia make use of your unique column, so that\n  # your lookups will benefit from the unique index\n  acts_as_paranoid column: :active, sentinel_value: true\n\n  def paranoia_restore_attributes\n    {\n      deleted_at: nil,\n      active: true\n    }\n  end\n\n  def paranoia_destroy_attributes\n    {\n      deleted_at: current_time_from_proper_timezone,\n      active: nil\n    }\n  end\nend\n```\n\n##### Destroying through association callbacks\n\nWhen 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:\n\n``` ruby\nclass Client \u003c ActiveRecord::Base\n  acts_as_paranoid\n\n  has_many :emails, dependent: :destroy\nend\n\nclass Email \u003c ActiveRecord::Base\n  acts_as_paranoid\n\n  belongs_to :client\nend\n```\n\nWhen we call `destroy` on the parent `client`, it will call `destroy` on all of its associated children `emails`:\n\n``` ruby\n\u003e\u003e client.emails.count\n# =\u003e 5\n\u003e\u003e client.destroy\n# =\u003e client\n\u003e\u003e client.deleted_at\n# =\u003e [current timestamp]\n\u003e\u003e Email.where(client_id: client.id).count\n# =\u003e 0\n\u003e\u003e Email.with_deleted.where(client_id: client.id).count\n# =\u003e 5\n```\n\nSimilarly, when we call `really_destroy!` on the parent `client`, then each child `email` will also have `really_destroy!` called:\n\n``` ruby\n\u003e\u003e client.emails.count\n# =\u003e 5\n\u003e\u003e client.id\n# =\u003e 12345\n\u003e\u003e client.really_destroy!\n# =\u003e client\n\u003e\u003e Client.find 12345\n# =\u003e ActiveRecord::RecordNotFound\n\u003e\u003e Email.with_deleted.where(client_id: client.id).count\n# =\u003e 0\n```\n\nHowever, 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:\n\n``` ruby\nclass Client \u003c ActiveRecord::Base\n  acts_as_paranoid\n\n  has_many :emails, dependent: :destroy\nend\n\nclass Email \u003c ActiveRecord::Base\n  belongs_to :client\nend\n\n\u003e\u003e client.emails.count\n# =\u003e 5\n\u003e\u003e client.destroy\n# =\u003e client\n\u003e\u003e Email.where(client_id: client.id).count\n# =\u003e 0\n\u003e\u003e Email.with_deleted.where(client_id: client.id).count\n# =\u003e NoMethodError: undefined method `with_deleted' for #\u003cClass:0x0123456\u003e\n```\n\n#### delete_all:\n\nThe gem supports `delete_all` method, however it is disabled by default, to enable it add this in your `environment` file\n\n``` ruby\nParanoia.delete_all_enabled = true\n```\nalternatively, you can enable/disable it for specific models as follow:\n\n``` ruby\nclass User \u003c ActiveRecord::Base\n  acts_as_paranoid(delete_all_enabled: true)\nend\n```\n\n## Acts As Paranoid Migration\n\nYou can replace the older `acts_as_paranoid` methods as follows:\n\n| Old Syntax                 | New Syntax                     |\n|:-------------------------- |:------------------------------ |\n|`find_with_deleted(:all)`   | `Client.with_deleted`          |\n|`find_with_deleted(:first)` | `Client.with_deleted.first`    |\n|`find_with_deleted(id)`     | `Client.with_deleted.find(id)` |\n\n\nThe `recover` method in `acts_as_paranoid` runs `update` callbacks.  Paranoia's\n`restore` method does not do this.\n\n## Callbacks\n\nParanoia 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\n\nFor example if you want to index your records in some search engine you can go like this:\n\n```ruby\nclass Product \u003c ActiveRecord::Base\n  acts_as_paranoid\n\n  after_destroy      :update_document_in_search_engine\n  after_restore      :update_document_in_search_engine\n  after_real_destroy :remove_document_from_search_engine\nend\n```\n\nYou can use these events just like regular Rails callbacks with before, after and around hooks.\n\n## License\n\nThis gem is released under the MIT license.\n","funding_links":[],"categories":["Ruby","Active Record","模型","Gems","ActiveRecord"],"sub_categories":["Omniauth","Soft Deletes and Versioning"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubysherpas%2Fparanoia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frubysherpas%2Fparanoia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubysherpas%2Fparanoia/lists"}