Ruby on Rails: Don't delete, tombstone

Mike Solomon

In many web apps, things need to be deleted (no way!). But actually deleting records from your database has some side effects that aren’t immediately obvious:

  • Undelete isn’t possible without additional work
  • In small, simple apps, you may not have any other record of deleted rows
  • You may be able to improve your product based on what has been deleted
  • Extra work is required to “reactivate” deleted users or other models with their former data attached

One easy way to deal with this is with tombstones. A tombstone is simply a column in a table that marks whether a given record has been deleted.

This means that nearly all queries will need to filter based on this column to make sure it isn’t reading “deleted” (tombstoned) data. How annoying!

Fortunately, in Rails it is easy to separate out this concern. We can define a mixin called tombstoneable such that any ActiveRecord model that mixes it in will automatically filter out deleted records by default, and add some easy methods to query for tombstoned records as well.

Make it work

Create a new file lib/mixins/tombstoneable.rb:

module Mixins::Tombstoneable
  extend ActiveSupport::Concern

  included do
    default_scope { where(deleted: false) }
    scope :include_deleted, -> { unscope(where: :deleted) }
    scope :deleted, -> { include_deleted.where(deleted: true) }
  end

  def destroy
    update_attribute(:deleted, true)
  end

  def delete
    destroy
  end

  def undelete
    assign_attributes(deleted: false)
  end

  def undelete!
    update_attribute(:deleted, false)
  end
end

Nice! The magic is in the included block. We augment the default scope to always look for records that have not been deleted. If we wish to include deleted records as well, we can use the include_deleted scope:

User.include_deleted.find(...)

Or if we wish to only select deleted (tombstoned) edges, we can do that too:

User.deleted.find(...)

Now mix it in

We never finished actually mixing this in to a model. Let’s assume we have a model called User. Only one line is needed to mix in the tombstoning behavior:

class User < ActiveRecord::Base
  include Mixins::Tombstoneable
  ...

And add the column

We also need to make sure the database has a column to track which records have been deleted. This will be necessary for each model we wish to make tombstonable. From the command line:

rails g migration AddDeletedToUser deleted:boolean

Then modify the generated file to add a default and make the column NOT NULL:

class AddDeletedToUser < ActiveRecord::Migration
  def change
    add_column :users, :deleted, :boolean, null: false, default: false
  end
end

And run the migration:

rake db:migrate

That’s it!

I have found this to be a useful practice. Let me know how your experiences go (or have gone previously) in the comments. Enjoy your not-quite-deleted records!

comments powered by Disqus