For the application I am working on right now, the ability to restore content that has been deleted is one of the requirements. A lot of people would just go ahead and add
acts_as_paranoid
or
is_paranoid
and be done with it, but I've had trouble with that approach before.
I've been
reading a lot about the trouble with "soft deletes" (flagging a record as deleted instead of deleting it). Using a plugin that monkey patches ActiveRecord can go a long way towards fixing thesee problems, but it's a leaky abstraction and will bite you in the ass in unexpected ways. For example, all your uniqueness validations (and indexes) become much more complicated.
That's why Jeffrey Chupp
decided to kill is_paranoid
and Rick Olson doesn't use
acts_as_paranoid
any more.
There are other problems too. If you delete a lot of records, and you keep them in the same table, your table can get quite large, and all your queries slow down. At this point you have to use partitioning or
partial indexes to get acceptable performance.
Alternatives to soft delete
In my reading, I found two alternatives to soft delete to be compelling.
The first was the suggestion to properly model your domain. Why do you want to delete a record? What does that
mean? Udi Dahan
puts it this way:
Orders aren’t deleted – they’re cancelled. There may also be fees incurred if the order is canceled too late.
Employees aren’t deleted – they’re fired (or possibly retired). A compensation package often needs to be handled.
Jobs aren’t deleted – they’re filled (or their requisition is revoked).
Keeping that in mind, what if the task at hand really is to delete the record? The other idea that I liked was to archive the records in another table.
The first Rails plugin I came across that implemented this was
acts_as_soft_deletable
which besides being misnamed doesn't appear to be actively maintained. The author even disavows the plugin somewhat for 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.
Then I found
acts_as_archive
which is more recently maintained and used in production for a major Rails website.
There was only one problem --
acts_as_archive
didn't support PostgreSQL. Fortunately, that was easy enough to fix.
Restoring deleted records with acts_as_archive
acts_as_archive
has the ability to restore a deleted record, but only that record, not associated records.
I was troubled by this at first, but after thinking about it I came to the conclusion that restoring a network of objects is an application-dependant problem. Here's one way to achieve it.
Imagine you have a model like this, with Posts having many Comments and Votes.
A Post can be deleted, and when it is, it should take the Comments and Votes with it:
class Post
acts_as_archive
has_many :votes, :dependent => :destroy
has_many :comments, :dependent => :destroy
end
(Assume Comment and Vote also have
acts_as_archive
.)
Now, I can restore a Post with its associated Votes and Comments like this:
def self.restore(id)
transaction do
Post.restore_all(["id = ?", id])
post = Post.find(id)
Vote.restore_all(Vote::Archive.all(:conditions => ["post_id = ?", id]).map(&:id))
Comment.restore_all(Comment::Archive.all(:conditions => ["post_id = ?", id]).map(&:id))
end
In my real code, I've broken apart the two pieces of this into a class method
restore
and an instance method
post_restore
which the freshly restored object uses to find its associated records and restore them.
post_restore
also takes care of post-restore tasks like putting the object back in the Solr index.
This all works great. But now let's say Comments can be deleted individually, and we want to restore them.
Here the logic is a little different, because a Comment can't be restored unless its parent Post still exists (unless it's being restored
by the Post, as above).
I take care of this logic in the administrative controller, by only showing child objects that it's valid to restore, and my foreign key constraints prevent anyone from getting around that.
I really wanted to delete that!
Sometimes you don't want to archive a deleted object. For example, in the application I'm working on, votes are canceled by re-voting. I don't want to save those votes -- there's no point, and it can even cause problems with restoring. Imagine having several archived votes from a user for a Post, and then deleting and restoring that Post. The restoration will try to bring back
all the votes. Again, I catch this with a uniqueness constraint, but I don't want it to happen in the first place.
Fortunately
acts_as_archive
has me covered.
To destroy a record without archiving it, you can use
destroy!
. Likewise for deleting, there is
delete_all!
.
http://railspikes.com/2010/2/26/acts-as-archive?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+RailSpikes+%28Rail+Spikes%29