Three old payphones

image by Pavan Trikutam

Find out what callbacks are defined on an Active Record model in the console

I was recently asked by my First #RubyFriend mentee how to list the callbacks present on an Active Record model. I didn’t know.

RailsConf 2024

I'm co-chairing RailsConf 2024 in Detroit May 7–9. Come and join us 

They were looking at a vast legacy codebase, with some large models defined across multiple files. The models used various gems, custom callbacks, and concerns. It was very hard to parse what behaviours the callbacks were causing.

I found mention of debugging callbacks in the Rails API documentation and, after a little digging, built a simple loop you can use to find the callbacks defined on an Active Record model.

Use…

…a loop in the Rails console.

Open the console.

bin/rails console

Paste this code into the console to see the user-defined callbacks on a model:

YourModel.__callbacks.each_with_object(Hash.new([])) do |(k, callbacks), result|
  next if k == :validate # ignore validations
  
  callbacks.each do |c|
    # remove autosaving callbacks from result
    next if c.filter.to_s.include?("autosave")
    next if c.filter.to_s.include?("_ensure_no_duplicate_errors")

    result["#{c.kind}_#{c.name}"] += [c.filter]
  end
end

For the User object in my application, which uses the Devise gem for authentication, the results look like this:

=> {
  "before_validation"=>[:downcase_keys, :strip_whitespace, :set_timestamps_for_agreements],
  "after_save"=>[#<Proc:0x00000001133b94d0 /.../gems/money-rails-1.15.0/lib/money-rails/active_record/monetizable.rb:148>],
  "before_save"=>[:ensure_authentication_token],
  "after_create"=>[:skip_reconfirmation_in_callback!],
  "before_create"=>[:generate_confirmation_token],
  "after_update"=>[:send_password_change_notification, :send_email_changed_notification],
  "before_update"=>[:clear_reset_password_token :postpone_email_change_until_confirmation_and_regenerate_confirmation_token],
  "after_commit"=>[:send_on_create_confirmation_instructions, :send_reconfirmation_instructions],
}

Here you can see the callbacks inserted into the User model based on our configuration of the devise gem. These aren’t listed in the app/models/user.rb file.

You can also see a dynamic Proc-based callback inserted by our use of the money-rails gem.

How does this work?

The #__callbacks method on a model is defined as a class method via Active Support. If accessed directly, like calling YourModel.__callbacks, it returns a complex hash structure containing the logic performed by the Active Record instance before or after changing its state.

The code above only pulls out the main method calls.

In the specific code above I’ve presumed the following:

You aren’t interested in seeing all of the validations. Code like validates_presence_of :attribute creates an entry in the callback chain. We skip displaying these with next if k == :validate.

You aren’t interested in the callbacks that automatically save the values of associated models where you have_many :related_models. Hence next if c.filter.to_s.include?("autosave").

Why?

When you’re new to a complex application this may be useful as a debugging tool.

I tend to limit my use of callbacks as much as possible due to the ease with which I‘ve confused myself before, but if you’re in a codebase that uses them a lot, this might get you up to speed with the existing behaviour more quickly.

Why not?

Please don’t use this code in production. It’s only for debugging and helping you to understand a new codebase.

I can’t think of a reason to use this in application code. Furthermore, the __ prefix in the method name indicates this is mostly based on Rails’s internal API and is subject to change.

Brighton Ruby 2024

Still running UK’s friendliest, Ruby event on Friday 28th June. Ice cream + Ruby 


Last updated on December 5th, 2022 by @andycroll

An email newsletter, with one Ruby/Rails technique delivered with a ‘why?’ and a ‘how?’ every two weeks. It’s deliberately brief, focussed & opinionated.