How to skip all callbacks for all models in Rails
Tags: ruby, rails, webdev
Published: 2023-11-03 04:07:10

## Problem

You want to have a way to skip all callbacks for all models in Rails.

## Before we begin

Two things:

1. Skip callbacks if this is a debugging session
    
2. Don't try to skip all callbacks in your business logic or production code. This is a strong hint for refactoring.
    

## Context

Let's assume you have the following model:

```ruby
class User < ApplicationRecord
  before_save :downcase_name
  after_save :add_title
  before_validation :ensure_username

  private

  def downcase_name
    self.name = name.downcase
  end

  def add_title
    self.title = "The #{name}"
  end

  def ensure_username
    self.username = self.email.split("@").first if username.blank?
  end
end
```

I used one single type of callback on `save`. But the solutions we will explore will try to skip all callbacks.

If you plan to use this I think you should first make sure that all callbacks work:

```ruby
require "test_helper"

class UserTest < ActiveSupport::TestCase
  def test_callback_changes_name_and_title
    user = User.new(name: "TEST", email: "username@domain.com")

    user.save

    assert_equal "test", user.name
    assert_equal "The test", user.title
    assert_equal "username", user.username
  end
end
```

## Solution 1: Using an `Suppressor`

I think this is the idiomatic way to achieve this in Rails if you need to suppress `create` and `destroy` callbacks -&gt; use [ActiveRecord::Suppressor](https://api.rubyonrails.org/classes/ActiveRecord/Suppressor.html) (thank you [Rob](https://x.com/robzolkos/status/1715356883198828709?s=20) for sharing this)

> ActiveRecord::Suppressor prevents the receiver from being saved during a given block

Here is how to use it:

```ruby
require "test_helper"

class UserTest < ActiveSupport::TestCase
  def test_suppressor_skip_callbacks
    user = User.new(name: "TEST", email: "username@domain.com")

    # 👇 An example of using User.supress
    User.suppress do
      user.save
    end

    assert_equal "TEST", user.name
    refute user.title
    refute user.username
  end
end
```

Looking at the [tests for it from Rails codebase](https://github.com/rails/rails/blob/main/activerecord/test/cases/suppressor_test.rb) it seems that Suppressor only works for save and create callbacks.

Here is a test that shows that it does not work on create do

```ruby
# app/models/user.rb
class User < ApplicationRecord
  before_destroy :before_destroy_action
 
  private

  def before_destroy_action
    raise "Can't destroy this record"
  end
end

# test/models/user_test.rb

require "test_helper"
class UserTest < ActiveSupport::TestCase
  def test_suppressor_does_not_work_on_destroy
    user = User.create(name: "TEST", email: "username@domain.com")

    User.suppress do
      # It confirms that it will raise RuntimeError 
      assert_raise RuntimeError do
        user.destroy
      end
    end
  end
end
```

So I will explore below two more methods to stop all callbacks. But again if you are only trying to stop the creation of other records from callbacks try to use this.

## Solution 2: Use conditional callbacks

You can use the following [feature from Rails](https://guides.rubyonrails.org/active_record_callbacks.html#using-if-and-unless-with-a-symbol):


![Rails Guides about conditional callbacks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aeualchud4z4afrdrc9s.png)


Here the solution will be to have an attribute that is by default `nil` or `false` and then define all callbacks with `unless :attribute` so that when we set that attribute to true, it will skip that callback:

```ruby
class User < ApplicationRecord
  attr_accessor :skip_callbacks

  before_save :downcase_name, unless: :skip_callbacks?
  after_save :add_title, unless: :skip_callbacks?
  before_validation :ensure_username, unless: :skip_callbacks?

  private

  def skip_callbacks?
    skip_callbacks
  end

  def downcase_name
    self.name = name.downcase
  end

  def add_title
    self.title = "The #{name}"
  end

  def ensure_username
    self.username = self.email.split("@").first if username.blank?
  end
end
```

Here is how you can use it for one model:

```ruby
require "test_helper"

class UserTest < ActiveSupport::TestCase
  def test_with_setting_skip_callbacks
    user = User.new(name: "TEST")
    user.skip_callbacks = true
    user.save

    assert_equal "TEST", user.name
    refute user.title
    refute user.username
  end
end
```

This solution is simple to add it to one model or to start using it if you start a greenfield project. But if you already have 1000 models, it will be painful to change callbacks on all models to change them to conditional callbacks. Extra pain points if they already have a condition defined.

*Advantage of this solution: It uses only public APIs/methods from Active Record so it should be safe to use and Rails upgrades will not break it.*

## Solution 3: Patching `ApplicationRecord` to allow removing all callbacks

For this solution, we will use [`reset_callback`](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-reset_callbacks) and [`set_callback`](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback) and patch `ApplicationRecord` to allow execution of something like `without_callbacks` with a block.

Let's first define this to work with one or multiple callbacks:

```ruby
# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  def self.without_callbacks(*callbacks)
    saved_callbacks = save_current_callbacks(callbacks)
    remove_callbacks(callbacks)
    yield
    restore_callbacks(saved_callbacks)
  end

  private

  def self.save_current_callbacks(callbacks)
    callbacks.map { |callback| [callback, self.__callbacks[callback]] }.to_h
  end

  def self.remove_callbacks(callbacks)
    callbacks.each { |callback| reset_callbacks(callback) }
  end

  def self.restore_callbacks(saved_callbacks)
    saved_callbacks.each { |callback, chain| set_callbacks(callback, chain) }
  end
end
```

and you can use it like this for one callback or many:

```ruby
require "test_helper"

class UserTest < ActiveSupport::TestCase
  def test_without_callbacks_does_not_change_name_or_title
    user = User.new(name: "TEST")

    User.without_callbacks(:save) do
      user.save
    end

    assert_equal "TEST", user.name
    refute user.title
  end

  def test_without_callbacks_does_not_change_name_title_or_username
    user = User.new(name: "TEST", email: "username@domain.com")

    User.without_callbacks(:save, :validation) do
      user.save
    end

    refute user.title
    refute user.username
    assert_equal "TEST", user.name
  end
end
```

And then if you want you can *add* one more thing to the `ApplicationRecord`:

```ruby
# app/models/application_record.rb

  def self.without_all_callbacks
    callbacks = self.__callbacks.keys
    without_callbacks(*callbacks) { yield }
  end
```

and then you can use this like this:

```ruby
require "test_helper"

class UserTest < ActiveSupport::TestCase
  def test_without_callbacks_does_not_change_name_title_or_username
    user = User.new(name: "TEST", email: "username@domain.com")

    User.without_all_callbacks do
      user.save
    end

    refute user.title
    refute user.username
    assert_equal "TEST", user.name
  end
end
```

### Warning

I would advise against using this solution because it uses private methods from ActiveRecord. These methods are open to be changed/removed and should be treated that way.

From all the methods I used in the code, only `reset_callbacks` is a public Ruby on Rails API. See it [on api.rubyonrails.org](https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-reset_callbacks). The others are methods not listed publicly on the api.rubyonrails.org. So they might change without any deprecation or warning.

### Improving on solution 3: Put the custom code in .`irbrc`

An intermediate solution would be not to pollute your application code with patching Active Record and put the patching into `.irbrc` file that you can put in the root of your Ruby on Rails app:

```ruby
# .irbrc

class ApplicationRecord < ActiveRecord::Base
  def self.without_all_callbacks
    callbacks = self.__callbacks.keys
    without_callbacks(*callbacks) { yield }
  end

  def self.without_callbacks(*callbacks)
    saved_callbacks = save_current_callbacks(callbacks)
    remove_callbacks(callbacks)
    yield
    restore_callbacks(saved_callbacks)
  end

  private

  def self.save_current_callbacks(callbacks)
    callbacks.map { |callback| [callback, self.__callbacks[callback]] }.to_h
  end

  def self.remove_callbacks(callbacks)
    callbacks.each { |callback| reset_callbacks(callback) }
  end

  def self.restore_callbacks(saved_callbacks)
    saved_callbacks.each { |callback, chain| set_callbacks(callback, chain) }
  end
end
```

So now when you start the Rails server the ApplicationRecord is not affected. So if you wrote those tests they will fail.

But if you open `rails c` then the `without_all_callbacks` and `without_callbacks` methods will be available on all your models.

### Checking before upgrade

If you decide to go for adding this kind of code, then I suggest you add the following test:

```ruby
require 'test_helper'

class ActiveRecordCallbacksTest < ActiveSupport::TestCase
  def setup
    @model = User.new
  end

  test "should have access to reset_callbacks" do
    assert(
      User.respond_to?(:reset_callbacks, true),
      "User models should have access to `reset_callbacks` to implement without_callbacks method"
    )
  end

  test "should have access to __callbacks hash" do
    assert_respond_to(
      @model,
      :__callbacks,
      "User model should have access to `__callbacks` hash to implement without_callbacks method"
    )
  end

  test "__callbacks should be a hash with keys as symbols to implement without_callbacks method" do
    callbacks = @model.__callbacks.keys
    assert_kind_of(
      Symbol,
      callbacks.first,
      "Expected `ApplicationRecords#__callbacks` should be a hash with keys as symbols"
    )
  end
end
```

And run this check when you upgrade your Rails version.

## Special case: Rails 7.1 `normalizes` method

⚠️ None of the techniques I presented above will skip [`normalizes`](https://api.rubyonrails.org/classes/ActiveRecord/Normalization/ClassMethods.html#method-i-normalizes) transformations.

If you have something like this in your models:

```ruby
class User < ApplicationRecord
  normalizes :email, with: -> email { email.strip.downcase }
  normalizes :name, with: -> name { name.strip.downcase }
end
```

Then skipping callbacks will not stop normalization from happening. Thus if you save the User record it will update the email and name as specified in the transformation

## Conclusion

In conclusion, skipping all callbacks for all models in Rails can be achieved using various methods such as **ActiveRecord::Suppressor**, **conditional callbacks**, or **patching ApplicationRecord**.

However, it is important to consider the potential drawbacks of each method and use them cautiously, especially in production code. Always test your solutions and be mindful of Rails version upgrades that may affect your implementation

---

Enjoyed this article?

Join my [**Short Ruby News**](https://shortruby.com/) **newsletter** for weekly Ruby updates from the community. For more Ruby **learning resources**, visit [**rubyandrails.info**](http://rubyandrails.info)**. You can also find me** [**on Ruby.social**](http://ruby.social) **or** [**Linkedin**](https://linkedin.com/in/lucianghinda) **or** [**Twitter**](https://x.com/lucianghinda)