18

I want to have a model where I need to soft delete a record and not show them in the find or any other conditions while searching.

I want to retain the model without deleting the record. How to go about this?

Jey Geethan
  • 2,235
  • 5
  • 33
  • 60
  • 3
    model and record are entirely different concepts. model is the template whereas record is the actual instance of it. – Prasad Surase Oct 05 '12 at 06:06
  • I am aware of that. I want to simplify the whole process for a particular model - which means whenever I soft delete a record, I wouldn't wanna see it in Model.all or anyother search queries unless I specify it. – Jey Geethan Oct 05 '12 at 06:12

11 Answers11

23

Just use a concern in rails 4

Example here

module SoftDeletable
  extend ActiveSupport::Concern


  included do
    default_scope { where(is_deleted: false) }
    scope :only_deleted, -> { unscope(where: :is_deleted).where(is_deleted: true) }
  end

  def delete
    update_column :is_deleted, true if has_attribute? :is_deleted
  end

  def destroy;
    callbacks_result = transaction do
      run_callbacks(:destroy) do
        delete
      end
    end
    callbacks_result ? self : false
  end

  def self.included(klazz)
    klazz.extend Callbacks
  end

  module Callbacks
    def self.extended(klazz)
      klazz.define_callbacks :restore
      klazz.define_singleton_method("before_restore") do |*args, &block|
        set_callback(:restore, :before, *args, &block)
      end
      klazz.define_singleton_method("around_restore") do |*args, &block|
        set_callback(:restore, :around, *args, &block)
      end
      klazz.define_singleton_method("after_restore") do |*args, &block|
        set_callback(:restore, :after, *args, &block)
      end
    end
  end

  def restore!(opts = {})
    self.class.transaction do
      run_callbacks(:restore) do
        update_column :is_deleted, false
        restore_associated_records if opts[:recursive]
      end
    end
    self
  end

  alias :restore :restore!

  def restore_associated_records
    destroyed_associations = self.class.reflect_on_all_associations.select do |association|
      association.options[:dependent] == :destroy
    end
    destroyed_associations.each do |association|
      association_data = send(association.name)
      unless association_data.nil?
        if association_data.is_deleted?
          if association.collection?
            association_data.only_deleted.each { |record| record.restore(recursive: true) }
          else
            association_data.restore(recursive: true)
          end
        end
      end
      if association_data.nil? && association.macro.to_s == 'has_one'
        association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
        association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
        Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
      end
    end
    clear_association_cache if destroyed_associations.present?
  end
end

Deletable

A rails concern to add soft deletes.

Very simple and flexible way to customise/ change

(You can change the delete column to be a timestamp and change the methods to call ActiveRecord touch ).

Best where you want to control code not have gems for simple tasks.

Usage

In your Tables add a boolean column is_deletable

class AddDeletedAtToUsers < ActiveRecord::Migration
  def change
    add_column :users, :is_deleted, :boolean
  end
end

In your models

class User < ActiveRecord::Base
  has_many :user_details, dependent: :destroy

  include SoftDeletable
end

Methods and callbacks available:

User.only_deleted
User.first.destroy
User.first.restore
User.first.restore(recursive: true)

Note: Focus Using update_column or touch if you decide to use a timestamp column.


Edited

If you are using rails <= 3.x (this example also use a DateTime field instead boolean), there are some differences:

module SoftDeletable
  extend ActiveSupport::Concern

  included do
    default_scope { where(deleted_at: nil }
    # In Rails <= 3.x to use only_deleted, do something like 'data = Model.unscoped.only_deleted'
    scope :only_deleted, -> { unscoped.where(table_name+'.deleted_at IS NOT NULL') }
  end

  def delete
    update_column :deleted_at, DateTime.now if has_attribute? :deleted_at
  end

  # ... ... ...
  # ... OTHERS IMPLEMENTATIONS ...
  # ... ... ...

  def restore!(opts = {})
    self.class.transaction do
      run_callbacks(:restore) do
        # Remove default_scope. "UPDATE ... WHERE (deleted_at IS NULL)"
        self.class.send(:unscoped) do
          update_column :deleted_at, nil
          restore_associated_records if opts[:recursive]
        end
      end
    end
    self
  end

  alias :restore :restore!

  def restore_associated_records
    destroyed_associations = self.class.reflect_on_all_associations.select do |association|
      association.options[:dependent] == :destroy
    end
    destroyed_associations.each do |association|
      association_data = send(association.name)
      unless association_data.nil?
        if association_data.deleted_at?
          if association.collection?
            association_data.only_deleted.each { |record| record.restore(recursive: true) }
          else
            association_data.restore(recursive: true)
          end
        end
      end
      if association_data.nil? && association.macro.to_s == 'has_one'
        association_class_name = association.options[:class_name].present? ? association.options[:class_name] : association.name.to_s.camelize
        association_foreign_key = association.options[:foreign_key].present? ? association.options[:foreign_key] : "#{self.class.name.to_s.underscore}_id"
        Object.const_get(association_class_name).only_deleted.where(association_foreign_key, self.id).first.try(:restore, recursive: true)
      end
    end
    clear_association_cache if destroyed_associations.present?
  end
end

Usage

In your Tables add a DateTime column deleted_at

class AddDeletedAtToUsers < ActiveRecord::Migration
  def change
    add_column :users, :deleted_at, :datetime
  end
end
Abs
  • 3,902
  • 1
  • 31
  • 30
  • i had to highjack your code a little bit to be also able to really destroy an object. also it would be able to include it just into activemodel and then let it decide if softdelete can be executed or not (in that case need to work the default scope) – Tim Kretschmer Jul 03 '19 at 04:20
21

Try this gem : https://github.com/technoweenie/acts_as_paranoid - ActiveRecord plugin allowing you to hide and restore records without actually deleting them

Mani dhayal
  • 270
  • 1
  • 3
9

Just add a boolean field called deleted or something to that effect. When you soft delete the record just set that field to true.

When doing a find just add that as a condition (or make a scope for it).

sosborn
  • 14,676
  • 2
  • 42
  • 46
5

The default_scope functionality in ActiveRecord 3 makes this easy, but personally, I favor the wide variety of standard solutions that can be dropped into the project. acts_as_archive in particular is the best fit for most of my projects, since it moves infrequently-accessed deleted records to a separate table, allowing the base table to stay small and in the database server's RAM.

Depending on your needs, you may also want to consider versioning instead of soft deletion.

willglynn
  • 11,210
  • 48
  • 40
4
  1. Add a date field to your model - deleted_at.
  2. Override the delete (or destroy) method on your model to set the deleted_at value. You can also create it as a new method. Something like soft_delete.
  3. Add a restore/undelete method to your model to set the deleted_at value back to null.
  4. Optional: create an alias method for the original delete (or destroy) method. Name it something like hard_delete.
yuяi
  • 2,617
  • 1
  • 23
  • 46
3

You can define a module like this

module ActiveRecordScope
  def self.included(base)
    base.scope :not_deleted, -> { base.where(deleted: false) }
    base.send(:default_scope) { base.not_deleted }
    base.scope :only_deleted, -> { base.unscope(where: :deleted).where(deleted: true) }

    def delete
      update deleted: true
    end

    def recover
      update deleted: false
    end
  end
end

Then in your class, you can write something like:

class User < ActiveRecord::Base
  include ActiveRecordScope

end

So you have both soft delete and recover.

You call user.delete to soft delete an user. Then you can call user.recover to set the deleted back to false again, and recover it.

Châu Hồng Lĩnh
  • 1,986
  • 1
  • 20
  • 23
1

Have a look at rails3_acts_as_paranoid.

A simple plugin which hides records instead of deleting them, being able to recover them.

...

This plugin was inspired by acts_as_paranoid and acts_as_active.

Usage:

class Paranoiac < ActiveRecord::Base
    acts_as_paranoid
    scope :pretty, where(:pretty => true)
end

Paranoiac.create(:pretty => true)

Paranoiac.pretty.count #=> 1
Paranoiac.only_deleted.count #=> 0
Paranoiac.pretty.only_deleted.count #=> 0

Paranoiac.first.destroy

Paranoiac.pretty.count #=> 0
Paranoiac.only_deleted.count #=> 1
Paranoiac.pretty.only_deleted.count #=> 1
Community
  • 1
  • 1
Benjamin Crouzier
  • 40,265
  • 44
  • 171
  • 236
1

If you use Rails4, Try this gem : https://github.com/alfa-jpn/kakurenbo

An association function of Kakurenbo is better than other gems.

solid
  • 66
  • 1
1

For Rails 4 don't use acts_as_paranoid (buggy for Rails 4), use paranoia. All you have to do is add a deleted_at timestamp column and include acts_as_paranoia in the model.

From there, just call destroy on the object and all other ActiveRecord relations and most other methods(like :count) automatically exclude the soft_deleted records.

JaTo
  • 2,742
  • 4
  • 29
  • 38
0

i wont use a default scope cause if i want to get all the records, i need to ignore it using "with_exclusive_scope" which in turn is messy. the would go by adding a 'deleted' boolean field which is set when the record is deleted. Also, would have added scopes to get the data as per the condition.

checkout Overriding a Rails default_scope and Rails: Why is with_exclusive_scope protected? Any good practice on how to use it?

Community
  • 1
  • 1
Prasad Surase
  • 6,486
  • 6
  • 39
  • 58
0

Add one column say status in your table and on deletion of records update the value of column status to inactive. and while fetching the records, add condition status != "inactive" in the query.

Deepika
  • 826
  • 6
  • 14