Skip to content

Commit

Permalink
Update the acts_as_paranoid comparison and the thanks section in the …
Browse files Browse the repository at this point in the history
…readme. Change some variable names in code to have a more consistent terminology.
  • Loading branch information
ajh committed May 19, 2008
1 parent 1ff227e commit e0f3946
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 30 deletions.
13 changes: 5 additions & 8 deletions README
Expand Up @@ -31,7 +31,7 @@ restored = Artist.find(34) # The artist is restored with all the same

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. The tradeoff is that the deleted table needs to be maintained along with the source table. For example, if the artists table adds a column in a future migration, then the deleted_artists table needs that column as well.
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 seperate table. The 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.

Expand Down Expand Up @@ -97,10 +97,7 @@ This was developed with Test::Unit in mind. Not sure how well it works with rspe

=== 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,
and acts_as_versioned, who's approach influenced this plugin.

=== TODO ===

make undestroying easier when there are lots of related rows to undestroy.
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
44 changes: 22 additions & 22 deletions lib/acts_as_soft_deletable.rb
Expand Up @@ -6,18 +6,18 @@ module SoftDeletable
module ClassMethods
def acts_as_soft_deletable
# don't allow multiple calls
return if self.included_modules.include?(Model::InstanceMethods)
return if self.included_modules.include?(Live::InstanceMethods)

include Model::InstanceMethods
extend Model::ClassMethods
include Live::InstanceMethods
extend Live::ClassMethods

model_class = self
live_class = self
@deleted_class = const_set("Deleted", Class.new(ActiveRecord::Base)).class_eval do
# class of undeleted model
cattr_accessor :model_class
# class of live (undeleted) model
cattr_accessor :live_class

self.model_class = model_class
self.set_table_name "deleted_#{model_class.table_name}"
self.live_class = live_class
self.set_table_name "deleted_#{live_class.table_name}"

extend Deleted::ClassMethods
include Deleted::InstanceMethods
Expand All @@ -28,10 +28,10 @@ def acts_as_soft_deletable
module Deleted

module ClassMethods
# Creates a deleted table by introspecting on the original table
# Creates a deleted table by introspecting on the live table
def create_table(create_table_options = {})
connection.create_table(table_name, create_table_options) do |t|
model_class.columns.select{|col| col.name != model_class.primary_key}.each do |col|
live_class.columns.select{|col| col.name != live_class.primary_key}.each do |col|
t.column col.name, col.type, :scale => col.scale, :precision => col.precision
end
t.datetime :deleted_at
Expand All @@ -43,11 +43,11 @@ def drop_table(drop_table_options = {})
connection.drop_table(table_name, drop_table_options)
end

# Updates the deleted table by adding or removing rows to match the original table.
# This is useful to call after adding or deleting columns in the original table.
# Updates the deleted table by adding or removing rows to match the live table.
# This is useful to call after adding or deleting columns in the live table.
def update_columns
original_specs = returning({}) do |h|
model_class.columns.each do |col|
live_specs = returning({}) do |h|
live_class.columns.each do |col|
h[col.name] = { :type => col.type, :scale => col.scale, :precision => col.precision }
end
end
Expand All @@ -59,28 +59,28 @@ def update_columns
end
deleted_specs.reject!{|k,v| k == "deleted_at"}

(original_specs.keys - deleted_specs.keys).each do |name|
(live_specs.keys - deleted_specs.keys).each do |name|
connection.add_column \
table_name,
name,
original_specs[name][:type],
original_specs[name].reject{|k,v| k == :type}
live_specs[name][:type],
live_specs[name].reject{|k,v| k == :type}
end

(deleted_specs.keys - original_specs.keys).each do |name|
(deleted_specs.keys - live_specs.keys).each do |name|
connection.remove_column table_name, name
end

self.reset_column_information
model_class.reset_column_information
live_class.reset_column_information
end
end

module InstanceMethods
# restore the model from deleted status. Will destroy the deleted record and recreate the original record
# restore the model from deleted status. Will destroy the deleted record and recreate the live record
def undestroy!
self.class.transaction do
model = self.class.model_class.new
model = self.class.live_class.new
self.attributes.reject{|k,v| k == 'deleted_at'}.keys.each do |key|
model.send("#{key}=", self.send(key))
end
Expand All @@ -92,7 +92,7 @@ def undestroy!
end
end

module Model
module Live

module ClassMethods
# returns instance of deleted class
Expand Down

0 comments on commit e0f3946

Please sign in to comment.