How to save a model without running callbacks in R

2019-02-05 18:56发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

update_column is an ActiveRecord function which does not run any callbacks, and it also does not run validation.



回答4:

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.



回答5:

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.



回答6:

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



回答7:

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