Hiding data in Rails with default_scope

One of ActiveRecord's less well known features is the default_scope class method. I've just added support for deleting cards to Agile Planner. Rather than destroy users' data immediately when they click the delete button, I wanted to give people an opportunity to recover from mistakes, but it doesn't feel like the right time to implement a full undo system just yet.

My solution was to hide cards from the user interface after they've been deleted, and to write a cron job that will periodically remove old deleted cards from the database.

Yes, this is a bit basic, but it's not worth me adding full undo/redo support yet (there are plenty of higher priority things to work on). If a user deletes a card by mistake, and they really need the data that's on it, they can always ask me to restore it from backup. Rather than digging through my backups I'll just be able to toggle a setting on the card to bring it back to life. And you never know, accidental deletion might be a very rare event. I'll improve it if I need to.

The relevant sections of my ActiveRecord models look like this:

class Iteration < ActiveRecord::Base
  has_many :cards

class Card < ActiveRecord::Base
  belongs_to :iteration
  default_scope where(deleted: false)

So to find all the cards that belong to an iteration and haven't been deleted I can just use the cards scope. It'll filter all the deleted cards out automatically:

iteration.cards  # <- no deleted cards here

If you want to get at the deleted cards, you can use the unscoped method to remove the default scope.

Card.unscoped { iteration.cards }  # <- also returns deleted cards

It's such a simple approach that it took me less than an hour to develop and deploy the deletion of cards into production.

A word of warning

The unscoped method isn't something that I'd advocate using very often. Opportunities for misleading yourself and your co-workers are legion, but when it's a good fit for a problem like this it comes in very handy. I really didn't want to trawl round the app to find all of the methods that iterate over cards and manually filter out the deleted ones.

I love feedback and questions -- please say hello on Twitter or leave a comment.