How to use PaperTrail for soft-deletion

February 28, 2014 ActiveRecord PaperTrail Ruby Ruby on Rails

PaperTrail is the leading Ruby gem for ActiveRecord versioning. A less known fact is that you can use it for “soft” deletion - an ability to restore deleted records.

A major benefit of using PaperTrail instead of a “flag-as-deleted” approach used by acts_as_paranoid and the like, is that deleted objects don’t pollute the main table. It’s not the best solution if soft deletion is all you need, but if you already use PaperTrail for versioning, you also get undeletion for free.

How undeletion with PaperTrail works

PaperTrail stores a version when a versioned object is deleted. It also provides a method, Version#reify, that creates a model object from data stored in the version object. Add a save to it, and you get undeletion:

version.reify.save!

Note that the object is restored with the same ID as before, which may be unwanted, in which case you can zero out the ID before saving and get an autogenerated one. But in most cases, preserving IDs is actually beneficial.

If you have more than one model that need to be undeleted, all of them have to be versioned and restored one by one. The associations would be restored automatically because IDs of the restored models are preserved.

Listing deleted objects using PaperTrail

To undelete anything, you need to get a list of deleted objects first. This can be done with a somewhat complicated Arel query:

PaperTrail::Version.
  where(event: 'destroy', item_type: 'MyModel'). # find all records of deletion
  joins("LEFT JOIN my_models ON item_id=my_models.id").
  where("my_models.id IS NULL"). # avoid showing deleted objects
  select("distinct item_id, *").order('versions.created_at DESC') # avoid duplicates

The third line of the query is used to hide versions of objects that are already restored. The fourth line hides duplicates of objects that were deleted multiple times. Both of these can be skipped if you delete the version when restoring the object, so it makes practical sense, but I didn’t try it myself.

As this is a regular Arel scope, you can paginate it when showing to the end user.

However, if you want to filter versions, say, by owner, the query becomes more complicated because model attributes are stored in a serialized hash inside the version object. You can either filter with an object LIKE '\n%owner_id=?%\n', or add an owner_id attribute to the version itself.

Buy Me a Coffee at ko-fi.com