Related
CODE
# Item Model
class Item < ActiveRecord::Base
attr_accessor :paid_amount
after_save :amount_processed?
def amount_processed?
if self.try(:paid_amount)
return true
else
return false
end
end
end
# Controller snippet
...
# params = {"paid_amount" => 10}
#item.assign_attributes(params)
if #item.valid?
#item.save
end
...
Currently the callback is not running, i.e., the code never checks amount_processed?. The reason this is happening is because paid_amount isn't a db attribute for Item. But that is by design. The question is ASSUMING this design were to stay, would there be a way for me to run a callback to check amount_processed? simply based on the fact that the attribute was passed? (i.e., if you run #item.paid_amount you'd get "10" after the #item.assign_attributes).
Note that the following callbacks will not work:
after_save or after_touch because as above, the paid_amount is never saved so the #item is never updated
after_find because this runs, by definition, before the attribute assignment. So with this validation, even though amount_processed? is checked, when it is checked, #item.paid_amount = nil
Would love to combine the two...
Since the question asks how to do this GIVEN current design, a perfectly acceptable answer is to say in the current design, it's not possible. The callback will only work if the attribute is actually updated. In that case, I already have 2 strategies to tackle this, the easiest of which being moving amount_processed? to the controller level so I can check the paid_amount after the assign_attributes. The other strategy is to have a Child of Item, but this is dependent on other info about the code that, for simplicity's sake, I have withheld.
Thanks!
Ook I think I have the answer here, thanks for the comments. Willem is right, in the current design, I can ensure amount_processed? is run by using a custom validation, changing the callback to:
validate :amount_processed?
However, doing so then makes the code a bit hacky, since I'm co-opting a validation to do the work of a callback. In other words, I would have to ensure amount_processed? always returned true (at end of the if statement; obviously other work would be done with paid_amount). There are some other considerations as well looking holistically at my code.
Given that, may change the design... but this was still a very helpful exercise
I need to calculate values when saving a model in Rails. So I call calculate_averages as a callback for a Survey class:
before_save :calculate_averages
However, occasionally (and initially I have 10k records that need this operation) I need to manually update all the averages for every record. No problem, I have code like the following:
Survey.all.each do |survey|
survey.some_average = (survey.some_value + survey.some_other_value) / 2.to_f
#and some more averages...
survey.save!
end
Before even running this code, I'm worried the calculate_averages is going to get called and duplicate this and probably even cause some problems with the way I'm doing things. Ok, so then I think, well I'll just do nothing and let calculate_averages get called and do its thing. Problem there is, first, is there a way to force callbacks to get called even if you made no changes to the record?
Secondly, the way averages are calculated it's far more efficient to simply not let the callbacks get called at all and do the averages for everything all at once. Is this possible to not let callbacks get called?
I believe what you are asking for can be achieved with ActiveSupport::Callbacks. Have a look at set_callback and skip_callback.
In order to "force callbacks to get called even if you made no changes to the record", you need to register the callback to some event e.g. save, validate etc..
set_callback :save, :before, :my_before_save_callback
To skip the before_save callback, you would do:
Survey.skip_callback(:save, :before, :calculate_average).
Please reference the linked ActiveSupport::Callbacks on other supported options such as conditions and blocks to set_callback and skip_callback.
To disable en-mass callbacks use...
Survey.skip_callback(:save, :before, :calculate_averages)
Then to enable them...
Survey.set_callback(:save, :before, :calculate_average)
This skips/sets for all instances.
update_column is an ActiveRecord function which does not run any callbacks, and it also does not run validation.
Doesn't work for Rails 5
Survey.skip_callback(:save, :before, :calculate_average)
Works for Rails 5
Survey.skip_callback(:save, :before, :calculate_average, raise: false)
https://github.com/thoughtbot/factory_bot/issues/931
If you want to conditionally skip callbacks after checking for each survey you can write your custom method.
For ex.
Modified callback
before_save :calculate_averages, if: Proc.new{ |survey| !survey.skip_callback }
New instance method
def skip_callback(value = false)
#skip_callback = #skip_callback ? #skip_callback : value
end
Script to update surveys
Survey.all.each do |survey|
survey.some_average = (survey.some_value + survey.some_other_value) / 2.to_f
#and some more averages...
survey.skip_callback(true)
survey.save!
end
Its kinda hack but hope will work for you.
Rails 5.2.3 requiring an after party script to NOT trigger model events, update_column(column_name, value) did the trick:
task.update_column(task_status, ReferenceDatum::KEY_COMPLETED)
https://apidock.com/rails/ActiveRecord/Persistence/update_column
hopefully this is what you're looking for.
https://stackoverflow.com/a/6587546/2238259
For your second issue, I suspect it would be better to inspect when this calculation needs to happen, it would be best if it could be handled in batch at a specified time where network traffic is at its trough.
EDIT: Woops. I actually found 2 links but lost the first one, apparently. Hopefully you have it fixed.
For Rails 3 ActiveSupport::Callbacks gives you the necessary control. You can reset_callbacks en-masse, or use skip_callback to disable judiciously like this:
Vote.skip_callback(:save, :after, :add_points_to_user)
…after which you can operate on Vote instances with :add_points_to_user inhibited
Going to simplify a bit here, but assume an app that has Users and UserRecords. A User must have one or more UserRecords. I want to limit the creation of UserRecords to a method in User, namely #create_new_user_record.
In other words, I don't want to allow UserRecord.new or UserRecords.create anywhere else in the application. I need to control the creation of these records, and perform some logic around them (for example, setting the new one current and any others to not current), and I don't want any orphaned UserRecords in the database.
I tried the after_initialize callback and checking if the object is new and raising an error there, but of course I do need to call UserRecord.new in User#create_new_user_record. If I could somehow flag in #create_new_user_record that I am calling new from that method, and pick that up in after_intialize, that would work, but how?
I might be over thinking it. I can certainly create a that method on User, and just 'know' to always call it. But others will eventually work on this app, and I will go away and come back to it as some point.
I suppose I could raise the error and just rescue from it in #create_new_user_record. Then at least, if another develop tries it elsewhere they will find out why I did it when they pursue the error.
Anyway, wondering what the Rails gurus here had to say about it.
super method is what you are looking for. Though you'll need some workaround (maybe simple check for value of option only you know about) to fit your needs
class User < ActiveRecord:Base
def .new(attributes = nil, options = {})
do_your_fancy_stuff
if option[:my_secret_new_method]
super # call AR's .new method and automatically pass all the arguments
end
end
Ok, here's what I did. Feel free to tell me if this is bad idea or, if it's an ok idea, if there's a better way. For what it's worth, this does accomplish my goal.
In the factory method in the User model, I send a custom parameter in the optional options hash defined on the new method in the API. Then I in the UserRecord#new override, I check for this parameter. If it's true, I create and return the object, otherwise I raise in custom error.
In my way of thinking, creating a UserRecord object any other way is an error. And a developer who innocently attempts it would be lead to explanatory comments in the two methods.
One thing that's not clear to me is why I need to leave off the options hash when I call super. Calling super with it causes the ArgumentError I posted in my earlier comment. Calling super without it seems to work fine.
class User < ActiveRecord::Base
...
def create_new_user_record
# do fancy stuff here
user_record = UserRecord.new( { owner_id: self.id, is_current: true }, called_from_factory: true )
user_record.save
end
...
end
class UserRecord < ActiveRecord::Base
...
def UserRecord.new(attributes = nil, options = {})
if options[:called_from_factory] == true
super(attributes)
else
raise UseFactoryError, "You must use factory method (User#create_new_user_record) to create UserRecords"
end
end
...
end
I know how to check an attribute for errors:
#post.errors[:title].any?
Is it possible to check which validation failed (for example "uniqueness")?
Recently I came across a situation where I need the same thing: The user can add/edit multiple records at once from a single form.
Since at validation time not all records have been written to the database I cannot use #David's solution. To make things even more complicated it is possible that the records already existing in the database can become duplicates, which are detected by the uniqueness validator.
TL;DR: You can't check for a specific validator, but you can check for a specific error.
I'm using this:
# The record has a duplicate value in `my_attribute`, detected by custom code.
if my_attribute_is_not_unique?
# Check if a previous uniqueness validator has already detected this:
unless #record.errors.added?(:my_attribute, :taken)
# No previous `:taken` error or at least a different text.
#record.errors.add(:my_attribute, :taken)
end
end
Some remarks:
It does work with I18n, but you have to provide the same interpolation parameters to added? as the previous validator did.
This doesn't work if the previous validator has written a custom message instead of the default one (:taken)
Regarding checking for uniqueness validation specifically, this didn't work for me:
#post.errors.added?(:title, :taken)
It seems the behaviour has changed so the value must also be passed. This works:
#post.errors.added?(:title, :taken, value: #post.title)
That's the one to use ^ but these also work:
#post.errors.details[:title].map { |e| e[:error] }.include? :taken
#post.errors.added?(:title, 'has already been taken')
Ref #34629, #34652
By "taken", I assume you mean that the title already exists in the database. I further assume that you have the following line in your Post model:
validates_uniqueness_of :title
Personally, I think that checking to see if the title is already taken by checking the validation errors is going to be fragile. #post.errors[:title] will return something like ["has already been taken"]. But what if you decide to change the error message or if you internationalize your application? I think you'd be better off writing a method to do the test:
class Post < ActiveRecord::Base
def title_unique?
Post.where(:title => self.title).count == 0
end
end
Then you can test if the title is unique with #post.title_unique?. I wouldn't be surprised if there's already a Rubygem that dynamically adds a method like this to ActiveRecord models.
If you're using Rails 5+ you can use errors.details. For earlier Rails versions, use the backport gem: https://github.com/cowbell/active_model-errors_details
is_duplicate_title = #post.errors.details[:title].any? do |detail|
detail[:error] == :uniqueness
end
Rails Guide: http://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors-errors-details
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!
One workaround is to set a variable in the class, and check its value in the after_save.
Check it first. (if var)
Assign it to a 'false' value before calling update_attribute.
call update_attribute.
Assign it to a 'true' value.
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.
Could you use the before_save callback instead?
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?
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
Check out how update_attribute is implemented. Use the send method instead:
send(name.to_s + '=', value)
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.
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
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 :)
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
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
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.
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.
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