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!