Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to simplify the soft delete process with Ruby on Rails?

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?

like image 686
Jey Geethan Avatar asked Oct 05 '12 06:10

Jey Geethan


People also ask

How do you do soft delete in rails?

If you only need a soft delete (no callbacks and no associations to delete), you can just leave out the define_callbacks line. After this, I created a new method for deleting a record. This will just update the deleted_at of a record with the current time. If you only need soft delete, you're pretty much done.

How do you perform a soft delete?

A common way to implement soft delete is to add a field that will indicate whether data has been deleted or not. This SQL command will permanently remove the product with id=1 from the table in the database.

What is difference between delete and soft delete?

Hard vs soft deletesA “hard” delete is when rows are deleted using DELETE FROM table WHERE ... A “soft” delete is when rows are deleted using UPDATE table SET deleted_at = now() WHERE ...


1 Answers

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
like image 116
Abs Avatar answered Oct 13 '22 16:10

Abs