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
Related
I have a model with an attribute car
I want to add a field car_created_at
that will have as a value the timestamp of the first time car actually assigned a value.
What is the proper way to do that? maybe after_update hook?
According to the trusty rails docs, after_update only runs on updates.
after_save runs both when created and updated. So I would use that.
And beware that just doing save in the hook will cause infinite call stack, meaning save will trigger after_save, in which save is called.
So you will have to use update_columns.
after_save :touch_car_created
def touch_car_created
if car_created_at.nil?
update_columns(car_created_at: Time.zone.now)
end
end
I would prefer to go with before_save because this callback get called before object creation and updation.
# This callback works only when car value has changed
# and car_created_at is nil.
before_save :assign_car_created_time, if: Proc.new { car_changed? && car_created_at.nil? }
private
# Keep it private so it won't be accessed publicly.
def assign_car_created_time
# We only need to assign the value
self.car_created_at = Time.zone.now
end
Hope this works for you.
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.
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
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.
Let us say that we have the model Champ, with the following attributes, all with default values of nil: winner, lose, coach, awesome, should_watch.
Let's assume that two separate operations are performed: (1) a new record is created and (2) c.the_winner is called on a instance of Champ.
Based on my mock code, and the observer on the model, what values are saved to the DB for these two scenarios? What I am trying to understand is the principles of how callbacks work within the context of Base.save operation, and if and when the Base.save operation has to be called more than once to commit the changes.
class Champ
def the_winner
self.winner = 'me'
self.save
end
def the_loser
self.loser = 'you'
end
def the_coach
self.coach = 'Lt Wiggles'
end
def awesome_game(awesome_or_not=false)
self.awesome = awesome_or_not
end
def should_watch_it(should=false)
self.should_watch = should
end
end
class ChampObserver
def after_update(c)
c.the_loser
end
def after_create(c)
c.the_coach
end
def before_create(c)
c.awesome_game(true)
c.should_watch_it(true) if c.awesome_game
end
end
With your example, if you called champ.winner on a new and unmodified instance of Champ, the instance of Champ would be committed to the DB and would look like this in the database:
winner: 'me'
awesome: true
should_watch: true
loser: nil
coach: nil
The after_create callback would be called if it is a new record, and if not, the after_update callback would (this is why loser would be nil if the instance was new). However, because they just call a setter method on the instance, they will only update the instance and will not commit more changes to the DB.
You could use update_attribute in your observer or model methods to commit the change, but unless you actually need to have the record in the database and then update it, it's wasteful. In this example, if you wanted those callbacks to actually set loser and coach in the database, it'd be more efficient to use before_save and before_create.
The Rails guides site has a good overview of callbacks here, if you haven't read it already.