Ivan
Ivan

Reputation: 103849

Using the after_save callback to modify the same object without triggering the callback again (recursion)

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!

Upvotes: 29

Views: 16716

Answers (13)

Rajesh Paul
Rajesh Paul

Reputation: 7009

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

Upvotes: 0

Mike
Mike

Reputation: 8963

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

Upvotes: 3

Brendon Muir
Brendon Muir

Reputation: 4612

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.

Upvotes: 0

serengeti12
serengeti12

Reputation: 5586

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.

Upvotes: 1

Walt Jones
Walt Jones

Reputation: 1328

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?

Upvotes: 11

Dan Halabe
Dan Halabe

Reputation:

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

Upvotes: 1

scottd
scottd

Reputation: 7474

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

Upvotes: 7

Patrick McKenzie
Patrick McKenzie

Reputation: 4076

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

Upvotes: 4

Ivan
Ivan

Reputation: 103849

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 :)

Upvotes: 2

Codebeef
Codebeef

Reputation: 43996

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.

Upvotes: 3

srboisvert
srboisvert

Reputation: 12739

Could you use the before_save callback instead?

Upvotes: 11

Groxx
Groxx

Reputation: 2499

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.

Upvotes: 13

Terry G Lorber
Terry G Lorber

Reputation: 2962

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

send(name.to_s + '=', value)

Upvotes: 6

Related Questions