Skip ActiveRecord callbacks

Callbacks are great and many business rules are often implemented in those callbacks to maintain data consistency and perform useful actions in app. As you might know, we can define several kinds of callbacks in ActiveRecord model, refer to the following guide for more detailed information: http://guides.rubyonrails.org/active_record_callbacks.html

But what if you want to prevent those callbacks from running when updating your model? Or you just need to skip a few callbacks from running while others are still working properly. Let me show you a few ways to do that:

1. Skip all callbacks

If you read the above link carefully, it also mentions how you can update model without executing any callbacks by using one of the following methods:

  • decrement
  • decrement_counter
  • delete delete_all
  • increment
  • increment_counter
  • toggle touch
  • update_column
  • update_columns
  • update_all
  • update_counters

I don't want to explain every method in details because I am sure you are not too lazy to look for their documentation on Google. Just be careful when using above methods as it passes through all validations & business rules, and can possibly lead to invalid data

2. Skip only validation callbacks

Passing validation: false to save method is a common way. When doing this, no validation callbacks will be executed.

3. Skip specific callback

This is a bit more complex and require a bit of work. Let's say that we have a model User like this:

class User < ActiveRecord::Base
  after_create :send_welcome_email
  before_save :create_default_username

  private
  def send_welcome_email
    #send welcome email to user
  end

  def create_default_username
    self.username = self.name.downcase.gsub(/\s+/, '_')
  end
end

We have defined two callbacks. One callback called send_welcome_email will be executed every time new user is created in database. Another callback is create_default_username which is used to set default username from name value of User model before saving it to database.

Let's say that we want to create sample data in our database with 1000 dummy users. Our script looks like this:

1000.times do |i|
  user = User.new
  user.email = "user#{i}@example.com"
  user.name = "User #{i}"
  user.save
end

We are using @example.com email for each users, and we of course do not want to send welcome to these dummy users. But still, we want to keep create_default_username running to make sure username is set correctly.

In order to achieve this, we are going to use skip_callback method provided in Rails 3+. The signature of this method is:

skip_callback(name, *filter_list, &block)

The first argument is the action triggering the callback (like create, save, update, destroy, etc), the second argument is the order of the callback (before or after), and the last argument is the name of the callback function.

Basic usage example:

User.skip_callback(:create, :after, :send_welcome_email)

But it is not done yet. You also need to set the callback back to the model after skipping it. This is to make sure that any later action on User model will execute our callbacks as usual. The syntax of set_callback function are the same as skip_callback.

User.set_callback(:create, :after, :send_welcome_email)

So we apply this to our script:

User.skip_callback(:create, :after, :send_welcome_email)

1000.times do |i|
  user = User.new
  user.email = "user#{i}@example.com"
  user.name = "User #{i}"
  user.save
end

User.set_callback(:create, :after, :send_welcome_email)

It is said that this method is complex as we always need to have set_callback after skip_callback. In order to avoid this, we can extend the ActiveSupport::Callbacks::ClassMethods module (the code is copied from original post here: http://jeffkreeftmeijer.com/2010/disabling-activemodel-callbacks/). Create a file config/initializers/without_callback.rb with the following content:

module ActiveSupport::Callbacks::ClassMethods
  def without_callback(*args, &block)
    skip_callback(*args)
    yield
    set_callback(*args)
  end
end

And you will be able to skip a callback like this:

User.without_callback(:create, :after, :send_welcome_email) do 
  1000.times do |i|
    user = User.new
    user.email = "user#{i}@example.com"
    user.name = "User #{i}"
    user.save
  end
end

4. Thread-safe solution (updated on Aug 5, 2019)

Using skip_callback has a small issue, it is not thread-safe. It means that if you have many threads using User model and we call skip_callback in one thread, it will be reflected in the other threads and the callback won't be called in all other threads until we call set_callback again.

To deal with this issue, we can create a virtual boolean attribute on the model and put the condition on the callback.

class User < ActiveRecord::Base
  after_create :send_welcome_email, unless: :skip_sending_welcome_email
  before_save :create_default_username

  attr_accessor :skip_sending_welcome_email

  private
  def send_welcome_email
    #send welcome email to user
  end

  def create_default_username
    self.username = self.name.downcase.gsub(/\s+/, '_')
  end
end


1000.times do |i|
  user = User.new
  user.skip_sending_welcome_email = true
  user.email = "user#{i}@example.com"
  user.name = "User #{i}"
  user.save
end

By doing this, the send_welcome_email callback is only skipped on specific User object. The downside of this method is that you have to create those attributes for every callback you want to skip.

Do you have any better way? Please share and leave comment. Happy coding!