views:

2257

answers:

10

Hi,

If I add an after_save callback to an ActiveRecord model, and on that callback I use update_attribute to change the object, the callback is called again, and so a 'stack overflow' occurs (hehe, couldn't resist).

Is it possible to avoid this behavior, maybe disabling the callback during it's execution? Or is there another approach?

Thanks!

+1  A: 

Check out how update_attribute is implemented. Use the send method instead:

send(name.to_s + '=', value)
Terry Lorber
A: 

But that doesn't save the object, and if I save it after that I'll have the same issue.

Currently what I'm doing is executing a SQL statement, which I don't like very much :(

ActiveRecord::Base.connection.execute "UPDATE table SET column = #{value} WHERE id = #{id};"
Ivan
+3  A: 

One workaround is to set a variable in the class, and check its value in the after_save.

  1. Check it first. (if var)
  2. Assign it to a 'false' value before calling update_attribute.
  3. call update_attribute.
  4. Assign it to a 'true' value.
  5. end

This way, it'll only attempt to save twice. This will likely hit your database twice, which may or may not be desirable.

I have a vague feeling that there's something built in, but this is a fairly foolproof way to prevent a specific point of recursion in just about any application. I would also recommend looking at the code again, as it's likely that whatever you're doing in the after_save should be done in before_save. There are times that this isn't true, but they're fairly rare.

Groxx
Awesome! I've searched for a built in approach too, but so far it seems there's none, but it would be great if you could set a special property to tell Rails to temporarily suspend that callback... your approach is kind of like that do, so thanks a lot!
Ivan
+3  A: 

Could you use the before_save callback instead?

srboisvert
+1  A: 

If you use before_save, you can modify any additional parameters before the save is completed, meaning you won't have to explicitly call save.

Mr. Matt
A: 

Thanks guys, the problem is that I update other objects too (siblings if you will)... forgot to mention that part...

So before_save is out of the question, because if the save fails all the modifications to the other objects would have to be reverted and that could get messy :)

Ivan
+1  A: 

This code doesn't even attempt to address threading or concurrency issues, much like Rails proper. If you need that feature, take heed!

Basically, the idea is to keep a count at what level of recursive calls of "save" you are, and only allow after_save when you are exiting the topmost level. You'll want to add in exception handling, too.

def before_save
  @attempted_save_level ||= 0
  @attempted_save_level += 1
end

def after_save
  if (@attempted_save_level == 1) 
     #fill in logic here

     save  #fires before_save, incrementing save_level to 2, then after_save, which returns without taking action

     #fill in logic here 

  end
  @attempted_save_level -= 1  # reset the "prevent infinite recursion" flag 
end
Patrick McKenzie
That's clever, thanks!
Ivan
+5  A: 

Also you can look at the plugin Without_callbacks. It adds a method to AR that lets you skip certain call backs for a given block. Example:

def your_after_save_func
  YourModel.without_callbacks(:your_after_save_func) do
    Your updates/changes
  end
end
ScottD
Didn't know about that plugin, it'll come in handy, thanks!
Ivan
A: 

I had this problem too. I need to save an attribute which depends upon the object id. I solved it by using conditional invocation for the callback ...

Class Foo << ActiveRecord::Base  
    after_save :init_bar_attr, :if => "bar_attr.nil?"    # just make sure this is false after the callback runs

    def init_bar_attr    
        self.bar_attr = "my id is: #{self.id}"    

        # careful now, let's save only if we're sure the triggering condition will fail    
        self.save if bar_attr
    end
Dan Halabe
A: 

I didn't see this answer, so I thought I'd add it in case it helps anyone searching on this topic. (ScottD's without_callbacks suggestion is close.)

ActiveRecord provides update_without_callbacks for this situation, but it is a private method. Use send to get access to it anyway. Being inside a callback for the object you are saving is exactly the reason to use this.

Also there is another SO thread here that covers this pretty well: http://stackoverflow.com/questions/632742/how-can-i-avoid-running-activerecord-callbacks

Walt Gordon Jones