Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Rails plugin that provides the ability to soft delete models

branch: master

This reverts an earlier commit which had unintended side-effects and …

…provides a cleaner alternative.

With test/unit being required rails apps trying to run ./script/runner would also
attempt to erroneously run the test suite, and could result in failure of cron jobs
that used the script/runner and specified an environment to run in.

It is better instead to:

  include Test::Unit::ActsAsDeleted

in your test helper.

This reverts my earlier commit : 69910d8

Signed-off-by: Andy Hartford <hartforda@gmail.com>
latest commit 925c11de2e
Glenn Rempe grempe authored April 28, 2009 ajh committed April 29, 2009
Octocat-spinner-32 lib
Octocat-spinner-32 test
Octocat-spinner-32 .gitignore
Octocat-spinner-32 COPYING
Octocat-spinner-32 README
Octocat-spinner-32 Rakefile
Octocat-spinner-32 init.rb
README
Copyright (C) 2008 Substantial and Andy Hartford <hartforda @ gmail.com>

== acts_as_soft_deletable

This plugin provides the ability to soft delete ActiveRecord models. When 
models are destroyed, they will be archived into a special deleted table. 
They can later be restored easily.

    class Artist < ActiveRecord::Base
      acts_as_soft_deletable # This will wrap the destroy method to provide soft delete 
                             # support and create a new ActiveRecord class called Artist::Deleted
    end

    model = Artist.find(34)
    model.destroy   # removes row from artists table, and adds a row to 
                    # deleted_artists table

   
    # The deleted record can be retrieved several ways
    deleted = Artist::Deleted.find(34)            # using the deleted class
    deleted = Artist.find_with_deleted(34)        # using the live class
    deleted = Artist.find_with_deleted_by_id(34)  # dynamic finds work too

    deleted.undestroy!  # adds the row back to the artists table, and removes 
                        # if from the deleted_artists table

    restored = Artist.find(34) # The artist is restored with all the same 
                               # information. The updated_at column will be 
                               # Time.now if it exists.

== Use with Rails 2.3+

Before using this with a new Rails 2.3 app, you may want to consider using the
new default_scope feature (or named_scopes) with a deleted_at flag. See
http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping
for a discussion about this.

== Compare to acts_as_paranoid

Acts_as_paranoid takes the approach of using a deleted_at flag in the models
table. If the deleted_at column has a value, the row is considered 'deleted'.
The problem with this approach is that all finds on the model have to exclude
'deleted' rows. This turns out be be a challenge. Acts_as_paranoid patches the
ActiveRecord internals to accomplish this, but it is fragile and could break
with future changes to ActiveRecord. Also, some of the more exotic finds
currently don't work (has_many :through with polymorphism as of March 2008) and
supporting them means running on an upgrade treadmill to keep up with the
evolution of ActiveRecord.

This plugin avoids these problems by allowing the row to be deleted and
archiving it into another table. The behavior of ActiveRecord::Base#Find
doesn't have to change which should mean that this plugin is more immune to
breaking due to ActiveRecord development. Queries on the live table will also
be faster in the case of lots of deleted rows, because they will be in a
separate table. 

The biggest tradeoff is that deleted rows aren't visible through active record
associations because they're in a different table. The only solution I could
think of was to write proxies for the associations. But it would involve so
much code that I figured it wasn't worth it.

Another tradeoff is that the deleted table needs to be maintained along with the
live table. For example, if the artists table adds a column in a future
migration, then the deleted_artists table needs that column as well.

A migration helper is available (see below) that will help keep the deleted
table in sync. Also, a unit test helper is provided (again, see below) which
adds unit tests to the model ensuring soft delete is working. If this is used,
the test will fail if the deleted table gets out of sync.

== Setup

=== Model

Any ActiveRecord class that wants the soft delete functionality should add
the following line to their class definition:

    class SomeModel < ActiveRecord::Base
      acts_as_soft_deletable
      ...

=== Migration

and setup the deleted table with the following migration:

    class AddActsAsSoftDeletable < ActiveRecord::Migration
      def self.up
        SomeModel::Deleted.create_table
      end

      def self.down
        SomeModel::Deleted.drop_table
      end
    end

Any changes to the original table (such as adding a column) should be reflected
in the deleted table. Use the update_columns method:

    class AddSkuColumn < ActiveRecord::Migration
      def self.up
        add_column 'items', 'sku', :string
        Item::Deleted.update_columns # will add sku column
      end

      def self.down
        remove_column 'items', 'sku'
        Item::Deleted.update_columns # will remove sku column
      end
    end

Note that update_columns will happily delete columns if asked. In this case a
warning will be issued when the migration is run alerting the developer to the
situation. This warning can be disabled, see:
ActiveRecord::Acts::SoftDeletable#remove_column_warning_enabled=

=== Unit tests

A model's soft delete capabilities can be easily unit tested by using this provided assert:

    def test_soft_delete_works
      # will run the model through a destroy and undestroy while making sure all values were saved
      assert_model_soft_deletes( items(:radar_detector) )
    end

Note : You may need to put the following in your test_helper.rb file if you see an error like 'NoMethodError: undefined method 'assert_model_soft_deletes'' when running your unit tests:

    require 'test/unit'
    include Test::Unit::ActsAsDeleted

This was developed with Test::Unit in mind. Not sure how well it works with rspec.

=== Thanks

* Substantial, my employer for letting me release this
* acts_as_paranoid and technoweenie, for a plugin I've got good years of use out of
* acts_as_versioned, who's approach influenced this plugin
* Danimal, for the feedback on rubyonrails-talk
Something went wrong with that request. Please try again.