acts_as_paranoid and acts_as_versioned

So you’re writing a web-based application and you want journalling: when database records change, you want to keep track of the changes so that you have an audit trail. There are many ways to do this. If your application is going to be dealing with a lot of data, it’s best to have a separate ‘versions’ table for each table, where you store the old versions of your records.A couple of plugins for Rails exist to make this easier. The acts_as_versioned plugin deals with journalling for updates to records. It does not journal deletes – that’s where acts_as_paranoid comes in. That plugin will put a timestamp in the deleted_at column rather than delete the record, and it overrides the various find methods to ignore records with a non-null deleted_at column.

The 2 plugins don’t work together all that well without some modification, as described by Flinn Mueller here.

His solution solves part of the problem – deleting a record no longer deletes the versioned history of the record. But it still leaves the original record in the main table, with a deleted_at timestamp. I want to move that ‘deleted’ record into the versions table, with accurate deleted_at timestamp.

Here’s how I modified Flinn’s code to do that:


module ActiveRecord
  module Acts
    module Versioned
      module ClassMethods
        def acts_as_paranoid_versioned(options = {})
          acts_as_paranoid
          acts_as_versioned options

          # Override the destroy method. We want deleted records to end up in the versioned table,
          # not in the non-versioned table.
          self.class_eval do
            def destroy()
              transaction {
                destroy_with_callbacks                 # call the acts_as_paranoid delete function
                tmp = self.class.find_with_deleted(id) # get the 'deleted' object
                tmp.save_version()                     # run it through acts_as_versioned's save_version()
                tmp.destroy_without_callbacks!         # and finally really destroy the original
              }
            end
          end

          # protect the versioned model
          self.versioned_class.class_eval do
            def self.delete_all(conditions = nil); return; end
          end
        end
      end
    end
  end
end

This code goes in config/environment.rb. As you can see it’s a bit of a hack, but it works. If you have suggestions to do this more elegantly, by all means leave a comment.

I think it’s time someone merges acts_as_paranoid into acts_as_versioned. Maybe I’ll look at that one day when I have more time – for now this will do.

This entry was posted in Rails. Bookmark the permalink.

3 Responses to acts_as_paranoid and acts_as_versioned

  1. Pingback: Latest Bookmarks on Ma.gnolia.com at Ivan Enviroman

  2. Pingback: All About Ruby

  3. Pingback: acts_as_paranoid and acts_as_versioned on Rails 3 | Off you go... into the purple yonder!

Leave a Reply