Reputation: 103849
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
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
Reputation: 8963
The trick is just to use #update_column
:
Additionally, it simply issues a single quick update query to the db.
http://apidock.com/rails/ActiveRecord/Persistence/update_columns
Upvotes: 3
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
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
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
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
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
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
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
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
Reputation: 2499
One workaround is to set a variable in the class, and check its value in the after_save.
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
Reputation: 2962
Check out how update_attribute is implemented. Use the send method instead:
send(name.to_s + '=', value)
Upvotes: 6