What Active Record callbacks are registered on some model?
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