34

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?

at.
  • 50,922
  • 104
  • 292
  • 461
  • 3
    If you have 10k records to update then why not bypass AR completely and just do a simple SQL UPDATE? Especially if you want to skip the callbacks. – mu is too short Jan 27 '14 at 02:05
  • @muistooshort - I simplified the calculations I need to do, but they are complex and depend on other records too. I need Ruby for this. – at. Jan 27 '14 at 02:14

8 Answers8

25

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.

vee
  • 38,255
  • 7
  • 74
  • 78
  • Why does registering a callback force the callback to get called even if you made no changes to it? – at. Jan 27 '14 at 02:08
  • Well change isn't the only event that could trigger the callbacks. Because an event is registered for a callback the callback is expected to get called isn't it? – vee Jan 27 '14 at 02:16
  • what other change could trigger a `before_save` callback? – at. Jan 27 '14 at 02:25
  • it is not the change that triggers the callback, it is the event. The event in this case is the `save` event. So whenever save or save! is called, the `*_save` callbacks are triggered. – rmcsharry Jul 10 '19 at 17:32
21

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.

remo
  • 651
  • 5
  • 14
rafroehlich2
  • 1,308
  • 12
  • 16
  • Seems like it would possibly not call callbacks even when I want them to (if a new record is created in the middle of recalculating averages)? – at. Jan 27 '14 at 02:08
  • You can use these two calls around your action that iterates over the collection. Temporarily disabling specific call_backs for performance. – rafroehlich2 Jan 27 '14 at 02:12
  • 1
    I understand the process, but doesn't that open me up to not calling callbacks when I need them to be called during that window of time? – at. Jan 27 '14 at 02:13
  • 4
    This is limited to the instance of Rails running your calculation. Anything happening anywhere else would not be affected. – rafroehlich2 Jan 27 '14 at 02:15
  • 13
    This isn't valid syntax for the `skip_callback` command. The first argument isn't the name of the method you've defined, it's one of the callback phases eg. `save`, `validate`, `commit`, etc. The correct syntax for this would be `Survey.skip_callback(:save, :before, :calculate_averages)` See: http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html – davidgoli Jan 05 '15 at 22:53
19

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

theUtherSide
  • 3,338
  • 4
  • 36
  • 35
Ahmad Hussain
  • 2,443
  • 20
  • 27
  • 3
    looks like `update_all` is similar and allows me to update multiple columns at once. – at. Jan 27 '14 at 02:07
  • yes, annoying that I have to make 15 sql `update` calls for each record. – at. Jan 27 '14 at 06:52
  • 2
    update_column just calls update_columns (plural). it is the right way to save without validation or callbacks if you want to skip *all* callbacks and validations. this is very useful for things like just flipping a boolean. also check out the method touch, which just updates the updated_at column. – pduey Jan 28 '14 at 12:40
15

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

dan987
  • 333
  • 3
  • 10
5

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.

kaydanzie
  • 386
  • 3
  • 7
  • 17
Swaps
  • 1,450
  • 24
  • 31
4

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

FlimFlam Vir
  • 1,080
  • 9
  • 15
3

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

altocumulus
  • 21,179
  • 13
  • 61
  • 84
Rafeeq
  • 231
  • 2
  • 3
2

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.

Community
  • 1
  • 1
Sherwyn Goh
  • 1,352
  • 1
  • 10
  • 19
  • The first link is to something completely unrelated. The 2nd link is informative though, looks like my best bet is to use the `survey.update_all(field: value)` mechanism as that doesn't invoke callbacks after updating. – at. Jan 27 '14 at 02:07