58

Let's say I've got 15 user ids in an array called user_ids.

If I want to, say, change all of their names to "Bob" I could do:

users = User.find(user_ids)
users.update_all( :name => 'Bob' )

This doesn't trigger callbacks, though. If I need to trigger callbacks on these records saving, to my knowledge the only way is to use:

users = User.find(user_ids)
users.each do |u|
  u.name = 'Bob'
  u.save
end

This potentially means a very long running task in a controller action, however.

So, my question is, is there any other better / higher performance / railsier way to trigger a batch update to a set of records that does trigger the callbacks on the records?

tshepang
  • 12,111
  • 21
  • 91
  • 136
Andrew
  • 42,517
  • 51
  • 181
  • 281
  • also maybe you can list more details about your problem, because sometimes there are common workarounds for such problems – iafonov Aug 03 '11 at 19:10
  • Well, specifically in the model I'm dealing with I have progressive validation, and I use a before_save callback to verify if certain pieces of information are present and set a "status" field on the model based on whether the information is complete or incomplete. – Andrew Aug 03 '11 at 19:46
  • This show which methods call callbacks etc. http://www.davidverhasselt.com/set-attributes-in-activerecord/ – Kris Sep 05 '18 at 10:10

3 Answers3

78

Instead of using each/find_each, try using update method instead:

models.update(column: value)

Which is just a wrapper for the following:

models.each{|x| x.update(column: value)}
Weston Ganger
  • 6,324
  • 4
  • 41
  • 39
untitled
  • 1,295
  • 3
  • 12
  • 21
24

Here's another way of triggering callbacks. Instead of using

models.update_all(params)

you can use

models.find_each { |m| m.update_attributes(params) }

I wouldn't recommend this approach if you're dealing with very large amounts of data, though.
Hope it helps!

maxhm10
  • 1,034
  • 9
  • 20
  • 3
    This isn't really the same thing as `update_all`. `map` will instantiate a Ruby object for every one of your models, and issue a separate SQL query for every one of your models. `update_all` will bundle everything up into one SQL query, and won't instantiate the objects - which is why it's much faster for large collections. But like @iafonov's answer mentioned, there's no way you can get both the speed of `update_all` AND the callbacks, because by definition you can't call the callbacks without instantiating the models. – GMA Jun 17 '14 at 12:28
  • Although it's worth noting that, if you do use an approach like this, it's probably better to use [`find_each`](http://apidock.com/rails/ActiveRecord/Batches/find_each) than `map` or `each`. – GMA Jun 17 '14 at 12:29
  • @GeorgeMillo I'm not sure I understand your first comment. You said that "you can't call the callbacks without instantiating the models", which is exactly what `map` does. Of course, this is just a workaround, it is not recommended for large databases where performance is key. Just answering the OP's question :). Now, can you post an example of this code using `find_each`? And why is it better than `map`? – maxhm10 Jun 18 '14 at 22:35
  • 7
    my point is that `map` *will* call the callbacks (at the cost of instantiating the models) whereas `update_all` won't call them. The advantage of `find_each` is that it processes your models in batches (default 1000 at a time) instead of loading all of your models into memory at the same time (which may give you a huge performance hit if you have thousands of models). `models.find_each { |m|.update_attributes(params) }` will have the same effect as `models.each { |m|.update_attributes(params) }` or `models.map { |m|.update_attributes(params) }` – GMA Jun 19 '14 at 08:25
  • (the only difference between `map` and `each` is the value they return. Also worth noting there's not much point using `find_each` if there's no chance that `models` will return a very large collection) – GMA Jun 19 '14 at 08:30
  • That is a lot more efficient that the above answer suggesting a `map` rather than a `find_each` – gogaz Jul 05 '21 at 07:49
22

No, to run callbacks you have to instantiate an object which is expensive operation. I think the only way to solve your problem is to refactor actions that you're doing in callback into separate method that could use data retrieved by select_all method without object instantiation.

iafonov
  • 5,152
  • 1
  • 25
  • 21