What Active Record callbacks are registered on some model?

22
May, 2017
Zoran Žabčić

You have just joined as a developer on a bigger project, or you're integrating your app with some rails engine, or you're doing something else that involves callbacks. Anyway, no matter if you love them or hate them, they are part of the application and getting to know and understand them would be a wise thing to do.

Active Record callbacks are hooks into object’s life cycle giving you an ability to execute particular code at certain moments. The most popular (read: those you first meet as a rails programmer) are probably before_save and after_save callbacks, but there are nineteen callbacks in total, which give you a lot of power to do stuff.

Although callbacks are easy to use, the important thing to know is that they are just a tool. Overusing callbacks could quickly lead to a complicated, hard to test, unmaintainable code, so use them responsibly.

Listing Active Record Callbacks

First, let’s create a new rails app so we can try some things. I called mine callback_app :). Move to your app root directory and run rails console. In a console, by typing:

$ ActiveRecord::Callbacks::CALLBACKS
=> [:after_initialize, :after_find, :after_touch, :before_validation, :after_validation, :before_save, :around_save, :after_save, :before_create, :around_create, :after_create, :before_update, :around_update, :after_update, :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback]

we get back a list of nineteen available Active Record callbacks which also have a corresponding callback macro method. It is also possible for you to define and register a custom callbacks using the define_model_callbacks method, but that’s beyond the scope of this article.

As you can see, there are three kinds of callbacks:

  • :before - runs before a particular event
  • :after - runs after the event
  • :around - runs before and after the event

We can list them, as well:

$ ActiveSupport::Callbacks::CALLBACK_FILTER_TYPES
=> [:before, :after, :around]

You can register a callback in four different ways using callback macros as:

  • method references (symbol) - recommended
  • callback objects - recommended
  • inline methods (using a proc) - when appropriate
  • and inline eval methods (using a string) - deprecated

One more feature of registering callbacks with callback macros is that they are inheritable, for example:

class Person < ActiveRecord::Base
  before_save :do_something_with_person
end

class Manager < Person
  before_save :do_something_with_manager
end

In this scenario when the save method is called on Manager instance, both :do_something_with_person and :do_something_with_manager are triggered.

Considering all this, how to find out which callbacks are registered on a particular object and in which order are they executed?

Debugging Active Record callbacks order

Callbacks are run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. Another exception is Transaction Callbacks after_commit and after_rollback which are executed in reverse order.

Let’s create a Post model in our app and register some callbacks on it. To make things faster, we’ll use rails model generator.

$ bin/rails g model Post title
$ bin/rails db:create
$ bin/rails db:migrate

Update your Post model with:

class Post < ApplicationRecord
  before_save :before_save_method_1
  before_save :before_save_method_2

  around_save :around_save_method_1
  around_save :around_save_method_2

  after_save :after_save_method_1
  after_save :after_save_method_2
  
  private

    def before_save_method_1; puts __method__; end
    def before_save_method_2; puts __method__; end

    def around_save_method_1
      puts __method__.to_s + " IN"
      yield
      puts __method__.to_s + " OUT"
    end 

    def around_save_method_2
      puts __method__.to_s + " IN"
      yield
      puts __method__.to_s + " OUT"
    end 

    def after_save_method_1; puts __method__; end
    def after_save_method_2; puts __method__; end
end

What we did here is we registered callbacks using callback macros (before_save, around_save, and after_save) with method references (:before_save_method_1, :before_save_method_2, around_save_method_1, etc.). We defined our callbacks as private methods at the bottom. Before and after callback methods just print out method name: puts __method__, and around callback methods print out method name with suffix IN, yield block and then print out the method name again, but this time with suffix OUT.

Back to the console! We can check which callbacks are registered on our class by calling _*_callbacks method on it. Just calling the _save_callbacks method on the Post will return an array of ActiveSupport::Callbacks::Callback objects, but we are only interested in which methods are run in the save callback chain for now, so we are going to  filter them out:

$ Post._save_callbacks.map(&:filter)
=> [:after_save_method_2, :after_save_method_1, :before_save_method_1, :before_save_method_2, :around_save_method_1, :around_save_method_2]

Here they are! The order is kinda funny, but at least we listed all of them out. Let’s filter them further by their kind (:before, :after, and :around).

$ Post._save_callbacks.select { |cb| cb.kind.eql?(:before) }.map(&:filter)
=> [:before_save_method_1, :before_save_method_2]

That is great, just as we expected... Let’s do the same for after_save:

$ Post._save_callbacks.select { |cb| cb.kind.eql?(:after) }.map(&:raw_filter)
=> [:after_save_method_2, :after_save_method_1]

Seems like the order is wrong, :after_save_method_1 is defined before :after_save_method_2, therefore, it should be run before :after_save_method_2? Let’s do another test, we can manually run save callbacks:

$ Post.new.run_callbacks(:save)
before_save_method_1
before_save_method_2
around_save_method_1 IN
around_save_method_2 IN
around_save_method_2 OUT
around_save_method_1 OUT
after_save_method_1
after_save_method_2
=> true

Ok, we can confirm that all callbacks are executed in correct order and :after_save_method_1 is run before :after_save_method_2.

What the heck is going on here? Well, the trick is in the run_callbacks method. It calls the before and around callbacks in the order they were set, yields the block (if any), and then runs the after callbacks in reverse order.

Finally, the interesting thing about around filters is that they are nested:

# around_save_method_1 IN
# - around_save_method_2 IN
# -- yield
# - around_save_method_2 OUT
# around_save_method_1 OUT

Conclusion

When you have a simple model with just a few or none registered callbacks it’s pretty much easy to spot which callbacks are registered on it and understand how it works. On the other hand, when you have a much more complicated situation, let’s say, you inherited a project from another developer/company with a lot of registered callbacks, defined associations, included gems that add their own callbacks, it can become very messy to understand what’s going on in an application. Knowing which callbacks and in which order are executed would be very useful.

I wrote a simple gem cb_list which is basically a rake task that prints out all registered callbacks for a given class. It will help you debugging Active Record callbacks.

Let's say we are integrating some CMS with Spree (Solidus) and Spree::Product is accessible from both sides. If we want to see which callbacks are registered on it, we can run rake task provided by cb_list gem:

$ bin/rails ar:callbacks:show[Spree::Product]

# Spree::Product Active Record Callbacks
# 
# 1. INITIALIZE
# ====================
# 
# after_initialize
# --------------------
# 1. ensure_master
# 2. build_first_node
# 3. on_initialize
# 
# 2. FIND
# ====================
# 
# after_find
# --------------------
# Empty
# 
# 3. TOUCH
# ====================
# 
# after_touch
# --------------------
# 1. touch_taxons
# 
# 4. VALIDATION
# ====================
# 
# before_validation
# --------------------
# 1. set_slug
# 2. normalize_slug
# 3. validate_master
# 4. set_translation
# 
# after_validation
# --------------------
# 1. unset_slug_if_invalid
# 2. _ensure_no_duplicate_errors
# 3. reflect_errors_messages
# 
# 5. SAVE
# ====================
# 
# before_save
# --------------------
# 1. autosave_associated_records_for_tax_category
# 2. autosave_associated_records_for_shipping_category
# 3. set_nodes_default_locale_and_fallback
# 4. before_save_collection_association
# 
# after_save
# --------------------
# 1. create_slug
# 2. run_touch_callbacks
# 3. update_pg_search_documents
# 
# around_save
# --------------------
# Empty
# 
# 6. CREATE
# ====================
# 
# before_create
# --------------------
# Empty
# 
# after_create
# --------------------
# 1. autosave_associated_records_for_slugs
# 2. autosave_associated_records_for_product_option_types
# 3. autosave_associated_records_for_option_types
# 4. autosave_associated_records_for_product_properties
# 5. autosave_associated_records_for_properties
# 6. autosave_associated_records_for_variant_property_rules
# 7. autosave_associated_records_for_variant_property_rule_values
# 8. autosave_associated_records_for_variant_property_rule_conditions
# 9. autosave_associated_records_for_classifications
# 10. autosave_associated_records_for_taxons
# 11. autosave_associated_records_for_product_promotion_rules
# 12. autosave_associated_records_for_promotion_rules
# 13. autosave_associated_records_for_master
# 14. autosave_associated_records_for_variants
# 15. autosave_associated_records_for_variants_including_master
# 16. autosave_associated_records_for_prices
# 17. autosave_associated_records_for_stock_items
# 18. autosave_associated_records_for_line_items
# 19. autosave_associated_records_for_orders
# 20. autosave_associated_records_for_variant_images
# 21. build_variants_from_option_values_hash
# 22. autosave_associated_records_for_nodes
# 23. autosave_associated_records_for_translations
# 
# around_create
# --------------------
# Empty
# 
# 7. UPDATE
# ====================
# 
# before_update
# --------------------
# Empty
# 
# after_update
# --------------------
# 1. autosave_associated_records_for_slugs
# 2. autosave_associated_records_for_product_option_types
# 3. autosave_associated_records_for_option_types
# 4. autosave_associated_records_for_product_properties
# 5. autosave_associated_records_for_properties
# 6. autosave_associated_records_for_variant_property_rules
# 7. autosave_associated_records_for_variant_property_rule_values
# 8. autosave_associated_records_for_variant_property_rule_conditions
# 9. autosave_associated_records_for_classifications
# 10. autosave_associated_records_for_taxons
# 11. autosave_associated_records_for_product_promotion_rules
# 12. autosave_associated_records_for_promotion_rules
# 13. autosave_associated_records_for_master
# 14. autosave_associated_records_for_variants
# 15. autosave_associated_records_for_variants_including_master
# 16. autosave_associated_records_for_prices
# 17. autosave_associated_records_for_stock_items
# 18. autosave_associated_records_for_line_items
# 19. autosave_associated_records_for_orders
# 20. autosave_associated_records_for_variant_images
# 21. autosave_associated_records_for_nodes
# 22. autosave_associated_records_for_translations
# 
# around_update
# --------------------
# Empty
# 
# 8. DESTROY
# ====================
# 
# before_destroy
# --------------------
# 1. #<Proc:0x007feb7a908cc0@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 2. #<Proc:0x007feb7a8f1890@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 3. #<Proc:0x007feb7a8d9768@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 4. #<Proc:0x007feb7a8c5a10@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 5. #<Proc:0x007feb7a888ea8@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 6. #<Proc:0x007feb7a86caa0@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 7. #<Proc:0x007feb793cf9a8@/Users/zzabcic/.rbenv/versions/2.4.1/lib/ruby/gems/2.4.0/gems/activerecord-5.0.2/lib/active_record/associations/builder/association.rb:140 (lambda)>
# 
# after_destroy
# --------------------
# 1 punch_slug
# 
# around_destroy
# --------------------
# Empty
# 
# 9. COMMIT
# ====================
# 
# after_commit
# --------------------
# Empty
# 
# 10. ROLLBACK
# ====================
# 
# after_rollback
# --------------------
# Empty