Acts As Restorable

SVN / Doc

During the last Philly on Rails Pub Nite, the topic came up of deleting records. It seems as though the general consensus is that models that are too important to Just-Delete, should probably have a deleted column...and every piece of logic everywhere else in your system will be built around this deleted flag.

Apparently, acts_as_paranoid does a reasonable job of getting around this. I'm (surprisingly enough) a little more partial to my acts_as_filterable implementation...as it seems as less of a HackLayer dealing with deleted flags.

Anyway, I've inherited a pretty large code base, where the convention was The Deleted Flag. Damn never every model implements it. "Even joining tables?" you ask. Even joining tables. That's where acts_as_filterable came from.

So, I've gotten the chance to re-write this beast. I wanted deleted rows to be out of my uber-clean database. Out. As in not there. But...it was kind of nice having sensitive data still being alive somewhere. I, ideally, wanted rows nuked from my model, but somehow "Not Dead." I've illustrated this thought in the following picture:

Not Dead

So, here's how she works: You destroy a row, it "Goes away." By which I mean, it gets serialized (along with all its dependencies) to XML, and put in another database with a timestamps, and who deleted it. Thanks to Plugin a Week's Routing Plugin, there's a simple controller to look at records recently nuked, and pull them back in to the live database.

Sound like a bit much? Possibly. Having a deleted column in your important models, and building code around that? That's just gross. That needs to stop. Here's some tests.

First ye test models:

class RestorableModel < ActiveRecord::Base
  acts_as_restorable

  has_many    :restorable_dependents, :dependent => :destroy
end

class RestorableDependent < ActiveRecord::Base
  belongs_to  :restorable_model
end


class ModelWithDatatype < ActiveRecord::Base
  acts_as_restorable
end

class UnrestorableModel < ActiveRecord::Base; end
Copy/Paste

And some test cases:

def test_simple_backup
  assert_equal 0, DeletedRecord.find(:all).size
  assert_equal 0, RestorableModel.find(:all).size

  good_record = RestorableModel.create(:name => 'Good Record')
  assert_equal 1, RestorableModel.find(:all).size

  good_record.destroy

  assert_equal 0, RestorableModel.find(:all).size
  assert_equal 1, DeletedRecord.find(:all).size
end

def test_alternate_data_types
  time_stamp = Time.now
  mwd = ModelWithDatatype.create(:int => 1, :bool => false, :tmstp => time_stamp)
  mwd.destroy

  assert_equal 1, DeletedRecord.find(:all).size
  rec = DeletedRecord.find(:first).restore

  assert_equal 1, rec.int
  assert_equal false, rec.bool

  assert rec.tmstp.is_a?(Time)
end

def test_dependents
  r = RestorableModel.create(:name => 'Parent Record')
  d = RestorableDependent.new
  r.restorable_dependents << d

  assert_equal 1, RestorableDependent.find(:all).size
  assert_equal 1, RestorableModel.find(:all).size
  assert_equal 1, r.restorable_dependents.size

  assert r.destroy

  assert_equal 0, RestorableDependent.find(:all).size
  assert_equal 0, RestorableModel.find(:all).size
end
Copy/Paste