28

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!

Ivan
  • 97,549
  • 17
  • 50
  • 58

13 Answers13

13

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
  • 2,489
  • 1
  • 25
  • 32
  • 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 Oct 19 '08 at 11:37
11

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: How can I avoid running ActiveRecord callbacks?

Community
  • 1
  • 1
Walt Jones
  • 1,293
  • 3
  • 14
  • 17
11

Could you use the before_save callback instead?

srboisvert
  • 12,679
  • 15
  • 63
  • 87
7

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
  • 7,364
  • 1
  • 24
  • 27
6

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

send(name.to_s + '=', value)
Terry G Lorber
  • 2,932
  • 2
  • 23
  • 33
4

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
  • 4,066
  • 24
  • 23
3

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.

Codebeef
  • 43,508
  • 23
  • 86
  • 119
2

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
  • 97,549
  • 17
  • 50
  • 58
2

The trick is just to use #update_column:

  • Validations are skipped.
  • Callbacks are skipped.
  • updated_at/updated_on are not updated.

Additionally, it simply issues a single quick update query to the db.

http://apidock.com/rails/ActiveRecord/Persistence/update_columns

Mike
  • 8,853
  • 3
  • 35
  • 44
1

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
1

Sometimes this is because of not specifying attr_accessible in models. When update_attribute wants to edit the attributes, if finds out they are not accessible and create new objects instead.On saving the new objects, it will get into an unending loop.

serengeti12
  • 5,205
  • 4
  • 23
  • 27
0

I had a need to gsub the path names in a block of text when its record was copied to a different context:

attr_accessor :original_public_path
after_save :replace_public_path, :if => :original_public_path

private

def replace_public_path
  self.overview = overview.gsub(original_public_path, public_path)
  self.original_public_path = nil

  save
end

The key to stop the recursion was to assign the value from the attribute and then set the attribute to nil so that the :if condition isn't met on the subsequent save.

Brendon Muir
  • 4,540
  • 2
  • 33
  • 55
0

You can use after_save in association with if as follows:

after_save :after_save_callback, if: Proc.new {
                                                //your logic when to call the callback
                                              }

or

after_save :after_save_callback, if: :call_if_condition

def call_if_condition
  //condition for when to call the :after_save_callback method
end

call_if_condition is a method. Define the scenario when to call the after_save_callback in that method

Rajesh Paul
  • 6,793
  • 6
  • 40
  • 57