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
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
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.
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:
The first Rails plugin I came across that implemented this was
There was only one problem --
Restoring deleted records with
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:
Now, I can restore a Post with its associated Votes and Comments like this:
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.
Fortunately
To destroy a record without archiving it, you can use
http://railspikes.com/2010/2/26/acts-as-archive?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+RailSpikes+%28Rail+Spikes%29
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.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.
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).
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 newThen I founddefault_scope
feature (ornamed_scopes
) with adeleted_at
flag.
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
No comments:
Post a Comment