Callback Recursion

Tue Apr 15 23:05:00 UTC 2008

I made a clumsy mistake with ActiveRecord callbacks recently. I fixed my mistake, but was left wondering if there is a better way to do it.

When I save a specific ActiveRecord object, I need to iterate through all the other instances of that model and flip a flag, saving each one. For example, let’s say I have a piece of functionality where users can create multiple tasks, but only one task can be active at any one time. So I have a model called Task and it has an attribute called active. When a user marks a task as active, I need to go through all their other tasks and make sure they are marked inactive.

Initially, I tried to do this by implementing an after_save callback on the model.

after_save :deactivate_other_tasks

def deactivate_other_tasks
  user.tasks.each do |task|
    task.update_attribute(:active, false)
  end
end

The problem might be obvious to most, however, I had to write it, run it, and watch it fail to figure out the obvious. As I iterate through each task, every time I call update_attribute, I’m triggering the same after_save callback. This little recursion journey continues until the memory wall is hit.

So I scrapped the callback approach and instead overrode Task#active=

def active=(arg)
  write_attribute(:active, arg) # go ahead and change the attribute of this object
  if (arg =~ /^true$/i) # case-insensitive match of just the string true
    user.tasks.select { |x| x != self }.each { |x| x.update_attribute(:active, false) }
  end
end

This works great. The problem I have with it is that somewhere down the road, it would be easy for someone not familiar with this model to add a before_save or after_save callback for some entirely different reason, and fall into the same recursion trap. I can add some comments at the top of the class

# Dear lord, whatever you do, do NOT add any callbacks triggered on
# save. You cannot comprehend the consequences.

But that’s just silly.

Is there a Railsy way of doing this? Is there a way to save an ActiveRecord object and selectively indicate that you want to skip all the callbacks in that one instance?

Tags: rails callbacks

Comments