Karol Galanciak - Ruby on Rails and Ember.js consultant

Rails Quick Tips: Temporarily Disabling Touching With ActiveRecord.no_touching

Touching ActiveRecord models is quite a common thing in most of the Rails applications, especially useful for cache invalidation. By default, it updates updated_at timestamp with the current time, Here’s a typical example of using touch in a model:

1
2
3
4
5
# app/models/photo.rb

class Photo < ApplicationRecord
  belongs_to :user, touch: true
end

Whenever a new photo is created, or the existing one is updated/destroyed, the updated_at attribute of the associated user will be updated with the current time. In the majority of the cases, this is the desired behavior (it’s one of those rare ActiveRecord callbacks that is not that bad ;)). However, it might happen that you may not want touch to be executed for some reason. Is there any built-in solution that could solve that problem?

Anatomy Of The Problem

Temporarily disabling touching can useful either for performance reasons (when updating a large number of records) or simply to prevent after_touch or after_commit from being executed multiple times. The latter might indicate that there is a deeper problem in the design as putting any important logic causing side-effects beyond the record’s internal state in those ActiveRecord callbacks can easily go south (especially if you trigger email notifications), but the reality is that a lot of Rails applications use those callbacks in such cases.

The Solution

Fortunately, a heavy refactoring or a rewrite is not necessary. Instead, we can take advantage of ActiveRecord.no_touching which temporarily disables touching inside the block.

Imagine that you need to update all photos belonging to some user and touch this user only after all photos are updated. Here’s how it could be handled:

1
2
3
4
5
6
7
8
9
10
11
12
user = User.find(user_id)

ActiveRecord::Base.transaction do
  User.no_touching do
    user.photos.find_each do |photo|
      # user won't be `touch`ed
      photo.update!(some_attributes)
    end
  end

  user.touch
end

If for some reason disabling touching is necessary for all models, you could just call it on ActiveRecord::Base:

1
2
3
4
5
6
7
8
9
10
11
12
user = User.find(user_id)

ActiveRecord::Base.transaction do
  ActiveRecord::Base.no_touching do
    user.photos.find_each do |photo|
      # no model will be `touch`ed
      photo.update!(some_attributes)
    end
  end

  user.touch
end

And that’s it!

Summary

ActiveRecord.no_touching is certainly a quick solution to a potentially tricky issue. However, it is also a dirty hack that indicates a potential problem with the design of the application that should be addressed sooner than later.

Comments