Thought it's an easy task however I have stuck a little bit with this issue:
Would like to update one of the attributes of the model whenever it's saved, thus having a callback in the model:
after_save :calculate_and_save_budget_contingency
def calculate_and_save_budget_contingency
self.total_contingency = self.budget_contingency + self.risk_contingency
self.save
# => this doesn't work as well.... self.update_attribute :budget_contingency, (self.budget_accuracy * self.budget_estimate) / 1
end
And the webserver shoots back with the message ActiveRecord::StatementInvalid (SystemStackError: stack level too deep: INSERT INTO "versions"
Which basically tells me that there is an infite loop of save to the model, after_save and then we save the model again... which goes into another loop of saving the model
Just stuck at this point of time on this model attribute calculation. If anyone has encountered this issue, and has a nice nifty/rails solution, please shoot me a message below, thanks
Change your code to following
before_save :calculate_and_save_budget_contingency
def calculate_and_save_budget_contingency
self.total_contingency = self.budget_contingency + self.risk_contingency
end
Reason for that is - if you run save in after_save you end up in infinite loop: a save calls after_save callback, which calls save which calls after_save, which...
In general it's wise you use after save only for changing associated models, etc.
Try before_save or before_validation, but don't include the .save
Related
Ok, stumbled upon this weirdness. I have this in my user model.
after_create :assign_role, :subscribe_to_basic_plan
def assign_role
self.role = 1
self.save
end
def subscribe_to_basic_plan
self.customer_id = "hello"
self.save
end
(code is simplified for illustration purposes)
When I create my user and check it in the console I get role: 1, customer_id: nil. But!, if I remove saving from the first callback everything works fine.
after_create :assign_role, :subscribe_to_basic_plan
def assign_role
self.role = 1
end
def subscribe_to_basic_plan
self.customer_id = "hello"
self.save
end
produces role: 1, customer_id: "hello". So seems like it only reads the first .save in the callbacks. I would like to understand what is the exact behaviour and why. I spent a lot of time trying to pinpoint this and wouldn't want to stumble on something similar again.
EDIT:
Maybe this is helpful. When I use self.save! in subscribe_to_basic_plan I get an error and the record is not saved at all. Putting self.save! in the assign_role doesn't change anything, so the problem is definitely with the second .save.
This answer is theoretical since I'd need to see the full model code to be sure.
Most likely your first save in assign_role is failing for some reason. When it fails and returns falls that causes rails to skip all callbacks after it. Then your second callback never runs at all.
Possible solutions in my preferred order:
Don't use callbacks. Have your controller set those values before you save the model.
Use before_create so you aren't doing 3 saves of the exact same model in a row.
Combine your two callbacks into one callback with only one save.
Save using save(validate: false) in case it is failing on validation.
I have a TreatmentEvent model. Here are the relevant parts:
class TreatmentEvent < ActiveRecord::Base
attr_accessible :taken #boolean
attr_accessible :reported_taken_at #DateTime
end
When I set the taken column, I want to set reported_taken_at if taken is true. So I tried an after_save callback like so:
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
I think update_attribute calls save, so that's causing the stack level too deep error. But using the after_commit callback is causing this to happen, too.
Is there a better way to conditionally update one column when another changes? This answer seems to imply you should be able to call update_attributes in an after_save.
Edit
This also happens when using update_attributes:
def set_reported_taken_at
self.update_attributes(reported_taken_at: Time.now) if self.taken?
end
As a note, stack level too deep generally means an infinite loop
--
In your case, the issue will almost certainly be caused by:
after_commit :set_reported_token_at
def set_reported_taken_at
self.update_attribute(:reported_taken_at, Time.now) if taken?
end
--
The problem is after_commit is going to try and save the reported_taken_at even if you've just saved a record. So you're going to go over the record again and again and again and again...
Often known as a recursive loop - it's used a lot in native development, but for request (HTTP) based apps, it's bad as it leads to a never-ending processing of your request
Fix
Your fix should be like this:
#model
before_save :set_reported_token_at
def set_reported_taken_at
self.reported_taken_at = Time.now if taken? #-> assuming you have a "taken" method
end
Can't you use a before_save? You can see if the other field value has changed and if so update this field. That way you just have one DB call.
Relevant Code: http://pastebin.com/EnLJUJ8G
class Task < ActiveRecord::Base
after_create :check_room_schedule
...
scope :for_date, lambda { |date| where(day: date) }
scope :for_room, lambda { |room| where(room: room) }
scope :room_stats, lambda { |room| where(room: room) }
scope :gear_stats, lambda { |gear| where(gear: gear) }
def check_room_schedule
#tasks = Task.for_date(self.day).for_room(self.room).list_in_asc_order
#self_position = #tasks.index(self)
if #tasks.length <= 2
if #self_position == 0
self.notes = "There is another meeting in
this room beginning at # {#tasks[1].begin.strftime("%I:%M%P")}."
self.save
end
end
end
private
def self.list_in_asc_order
order('begin asc')
end
end
I'm making a small task app. Each task is assigned to a room. Once I add a task, I want to use a callback to check to see if there are tasks in the same room before and or after the task I just added (although my code only handles one edge case right now).
So I decided to use after_create (since the user will manually check for this if they edit it, hence not after_save) so I could use two scopes and a class method to query the tasks on the day, in the room, and order them by time. I then find the object in the array and start using if statements.
I have to explicitly save the object. It works. But it feels weird that I'm doing that. I'm not too experienced (first app), so I'm not sure if this is frowned upon or if it is convention. I've searched a bunch and looked through a reference book, but I haven't see anything this specific.
Thanks.
This looks like a task for before_create to me. If you have to save in your after_* callback, you probably meant to use a before_* callback instead.
In before_create you wouldn't have to call save, as the save happens after the callback code runs for you.
And rather than saving then querying to see if you get 2 or more objects returns, you should be querying for one object that will clash before you save.
In psuedo code, what you have now:
after creation
now that I'm saved, find all tasks in my room and at my time
did I find more than one?
Am I the first one?
yes: add note about another task, then save again
no: everything is fine, no need to re-save any edits
What you should have:
before creation
is there at least 1 task in this room at the same time?
yes: add note about another task
no: everything is fine, allow saving without modification
Something more like this:
before_create :check_room_schedule
def check_room_schedule
conflicting_task = Task.for_date(self.day)
.for_room(self.room)
.where(begin: self.begin) # unsure what logic you need here...
.first
if conflicting_task
self.notes =
"There is another meeting in this room beginning at #{conflicting_task.begin.strftime("%I:%M%P")}."
end
end
I've got a model which has a video attached with Paperclip. After it saves I use the saved video to generate a thumbnail. I need to do this after every save, even when a new video hasn't been uploaded, because the user can change the time where the thumbnail is captured.
I am currently using after_post_process to do this, but it will only generate the thumbnail when uploading a file (this is a callback which is part of Paperclip).
I would ideally use an after_save callback like this:
after_save :save_thumbnail
def save_thumbnail
#generate thumbnail...
self.update_attributes(
:thumbnail_file_name => File.basename(thumb),
:thumbnail_content_type => 'image/jpeg'
)
end
Unfortunately update_attributes calls save, which then calls the before_save callback causing an infinite loop. Is there a simple way to circumvent this behaviour?
Any update_attribute in an after_save callback will cause recursion, in Rails3+.
What should be done is:
after_save :updater!
# Awesome Ruby code
# ...
# ...
private
def updater!
self.update_column(:column_name, new_value) # This will skip validation gracefully.
end
Here is some documentation about it: https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
You could wrap it in a conditional, something like:
def save_thumbnail
if File.basename(thumb) != thumbnail_file_name
self.update_attributes(
:thumbnail_file_name => File.basename(thumb),
:thumbnail_content_type => 'image/jpeg'
)
end
end
That way it would only run once.
Rails 2:
Model.send(:create_without_callbacks)
Model.send(:update_without_callbacks)
Rails 3:
Vote.skip_callback(:save, :after, :add_points_to_user)
See this question:
How to skip ActiveRecord callbacks?
You can(and should) check if you actually need to update the thumbnail:
after_save :save_thumbnail
def save_thumbnail
if capture_time_changed? #assuming capture_time contains time when the thumbnail has to be captured
#generate thumbnail...
self.update_attributes(
:thumbnail_file_name => File.basename(thumb),
:thumbnail_content_type => 'image/jpeg'
)
end
end
Here you can read more about 'dirty' attributes: http://apidock.com/rails/ActiveRecord/Dirty
Although I'm not sure if it still can see the attribute changes in after_save. You can use a member variable to indicate changes in case it can't.
You can run it as a before_save instead.
After it has been validated, update the thumbnail, then let it go on to be saved, but just use the assignment methods
before_save :save_thumbnail
def save_thumbnail
self.thumbnail_file_name = File.basename(thumb),
self.thumbnail_content_type = 'image/jpeg'
end
Since that won't call save, you wont recurse, but it will immediately be saved after the method exits.
Something like that should work, unless there is an explicit reason you need it in after save.
Since you are not updating a separate object, but the same one, this will save you a database call as well. This is How i do timestamps and things like that too.
I have a model with a callback that runs after_update:
after_update :set_state
protected
def set_state
if self.valid?
self.state = 'complete'
else
self.state = 'in_progress'
end
end
But it doesn't actually save those values, why not? Regardless of if the model is valid or not it won't even write anything, even if i remove the if self.valid? condition, I can't seem to save the state.
Um, this might sound dumb, do I need to run save on it?
update
Actually, I can't run save there because it results in an infinite loop. [sighs]
after_update is run after update, so also after save. You can use update_attribute to save this value, or just call save (I'm not sure if there don't be any recurence). Eventualy you can assign it in before_update (list of availble options is here). On the other side invalid object will not be saved anyway, so why you want to assign here the state?
Judging by the fact that the examples in ActiveRecord documentation do things like this:
def before_save(record)
record.credit_card_number = encrypt(record.credit_card_number)
end
def after_save(record)
record.credit_card_number = decrypt(record.credit_card_number)
end
you do need to save the record yourself.
after_update works on the object in memory not on the record in the table. To update attributes in the DB do the following
after_update :set_state
protected
def set_state
if self.valid?
self.update_attribute('state', 'complete')
else
self.update_attribute('state', 'in_progress')
end
end