Rails: around_* callbacks
around_*
callbacks are invoked before the action, then when you want to invoke the action itself, you yield
to it, then continue execution. That's why it's called around
The order goes like this: before
, around
, after
.
So, a typical around_save
would look like this:
def around_save
#do something...
yield #saves
#do something else...
end
Use ActiveSupport Around-Callbacks to execute code (tracing/logging) around when my services are invoked?
The cleanest way would be to make use of prepended.
It'll look like this:
module Traceable
extend ActiveSupport::Concern
prepended do
include ActiveSupport::Callbacks
define_callbacks :trace
set_callback :trace, :around do |_r, block|
puts "start the trace"
block.call
puts "end the trace"
end
end
def perform
run_callbacks :trace do
super
end
end
end
module Users
class CreateService
prepend Traceable
attr_accessor :name
def initialize(name)
@name = name
end
def perform
# code to create the user
puts "perform"
end
end
end
The link has more details. The prepend
in the service means that Users::CreateService.perform
will first look for its definition in the Traceable
module. That allows Traceable
to wrap perform
in the callback(s).
Since we're using prepend
instead of include
in the service, we need to call define_callbacks
and set_callback
inside of ActiveSupport::Concern
's prepended
block.
If you prepend
the Traceable
module in your service, you don't really need callbacks
or concerns
at all. The Traceable
module could just be this, and it would have the same outcome:
module Traceable
def perform
puts "before"
super
puts "after"
end
end
module Users
class CreateService
prepend Traceable
...
end
end
Rails around_action in the callback stack
around_action
are more like append_before_action
+ prepend_after_action
.
Internally, think of it like rails
has two arrays, @before_actions
and @after_actions
. So when you declare around_action
, it pushes/appends it to the end of @before_actions
and it unshift/prepends to the @after_actions
.
With a quick test as follows:
class SomeController < ApplicationController
before_action :before_action
after_action :after_action
around_filter :around_action
def before_action
$stderr.puts "From before_action"
end
def after_action
$stderr.puts "From after_action"
end
def around_action
begin
$stderr.puts "From around_action before yielding"
yield
$stderr.puts "From around_action after yielding"
end
end
def index
end
end
I got the following in the log:
Started GET "/" for 127.0.0.1 at 2016-03-21 17:11:01 -0700
Processing by SomeController#index as HTML
From before_action
From around_action before yielding
Rendered some/index.html.slim within layouts/index (1.5ms)
From around_action after yielding
From after_action
How to have two callbacks in around_destroy in Rails?
It'll work without any issue, as Active Record wraps-up these callback methods in a single transaction.
Since the object is destroyed in first yield, it seems the later is not feasible (assuming everything is running in one thread). How Rails handles this?
No, object isn't destroyed in first yield. Object is destroyed only after every callback method(except after_commit/after_rollback) has ran successfully.
Here's a quick example to illustrate this.
Class User < ActiveRecord::Base
around_destroy :callback1, :callback2
after_commit :after_commit_callback
def callback1
puts "Inside First callback, before yield"
yield
puts "Inside First callback, after yield"
end
def callback2
puts "Inside Second callback, before yield"
yield
puts "Inside Second callback, after yield"
end
def after_commit_callback
puts "after commit callback message"
end
end
Here's the required console commands:
[14] pry(main)> u = User.create(email: "rahul@example.com", password: "testing")
(0.3ms) BEGIN
User Exists (0.4ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = BINARY 'rahul@example.com' LIMIT 1
SQL (0.4ms) INSERT INTO `users` (`created_at`, `email`, `encrypted_password`, `updated_at`) VALUES ('2015-06-22 13:01:12', 'rahul@example.com', '$2a$10$h5TYOd20JosN0oVa7ufK.OU3PnHJRi/X6CcTxy7UuDOqYLCIB...u', '2015-06-22 13:01:12')
(25.8ms) COMMIT
after commit callback message
=> #<User id: 8, email: "rahul@example.com", encrypted_password: "$2a$10$h5TYOd20JosN0oVa7ufK.OU3PnHJRi/X6CcTxy7UuDO...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil, name: nil, created_at: "2015-06-22 13:01:12", updated_at: "2015-06-22 13:01:12">
[15] pry(main)> u.destroy
(0.3ms) BEGIN
Inside First callback, before yield
Inside Second callback, before yield
SQL (0.4ms) DELETE FROM `users` WHERE `users`.`id` = 8
Inside Second callback, after yield
Inside First callback, after yield
(4.0ms) COMMIT
after commit callback message
=> #<User id: 8, email: "rahul@example.com", encrypted_password: "$2a$10$h5TYOd20JosN0oVa7ufK.OU3PnHJRi/X6CcTxy7UuDO...", reset_password_token: nil, reset_password_sent_at: nil, remember_created_at: nil, sign_in_count: 0, current_sign_in_at: nil, last_sign_in_at: nil, current_sign_in_ip: nil, last_sign_in_ip: nil, name: nil, created_at: "2015-06-22 13:01:12", updated_at: "2015-06-22 13:01:12">
How do ActiveRecord Callbacks actually Work in Rails
ActiveRecord and ActiveModel use ActiveSupport::Callbacks to do their dirty work.
If you take a look at its ClassMethods module, you'll find define_callbacks
which is what defines (through module_eval
) _run_update_callbacks
and friends. The _run_*_callbacks
methods just calls run_callbacks
from the main module.
So to answer your questions:
I believe ActiveRecord actually triggers the callbacks in the code you posted. Looks like ActiveRecord::Transactions has a couple that it runs (the transaction related ones, funny enough).
Without digging too deep, it looks like the
run_callbacks
method just keeps a list of all the callbacks and then goes through and figures out what's what and what to do.
Probably not as in depth of an answer as you were hoping for, but hopefully this can at least get you going in the right direction digging around and investigating on your own.
Callbacks order in Rails
ActiveRecord::Callbacks documents the order in "Ordering callbacks".
Non-transactional callbacks are executed in the order the are defined.
class Topic < ActiveRecord::Base
after_save :log_children
after_save :do_something_else
...
end
When a Topic is saved, log_children will be executed, then do_something_else.
Transactional callbacks (after_commit, after_rollback) are the opposite, the last defined transactional callback is executed first.
class Topic < ActiveRecord::Base
after_commit :log_children
after_commit :do_something_else
...
end
When a Topic is committed, first do_something_else runs, then log_children.
If there's any doubt, you can combine them into a single callback.
class Topic < ActiveRecord::Base
after_commit :commit_callback
private def commit_callback
log_children
do_something_else
end
...
end
ActiveRecord before_update callback understanding
Yes. Thats exactly what will happen.
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
assign_attributes(attributes)
save
end
end
See:
- https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/Persistence.html#method-i-update
- https://api.rubyonrails.org/classes/ActiveModel/AttributeAssignment.html
Why are my Rails-like callbacks around Ruby methods not working?
When you use a class instance variable (@before_actions
) and self.before_actions
uses ||=
your code works; fine.
When self.before_actions
uses =
instead of ||=
your code fails because every time you call before_actions
it resets @before_actions
to []
. No callback will stay defined long enough to do anything.
Your version of your code that uses a class variable (@@callbacks
) sort of works because you're initializing @@callbacks
only once outside the accessor. However, you'll have problems as soon as you have two subclasses of Callbacks
: Callbacks
and its subclasses will all share the same @@callbacks
, so you won't be able to have different subclasses with different callbacks.
Related Topics
How to Wrap Link_To Around Some HTML Ruby Code
How to Clear the Terminal in Ruby
Rails Paperclip How to Delete Attachment
In Rails, How to Add a New Method to String Class
How to Sort an Array in Ruby to a Particular Order
What's the Precedence of Ruby'S Method Call
Has Anyone Tried Installing Ruby & Rubygems from Source on Ubuntu (Preferably Ubuntu 9)
Ruby on Rails: Advanced Search
What Does the '&' Mean in the Following Ruby Syntax
Uninstalling All Gems Ruby 2.0.0
Rails Browser Detection Methods
Passing a Method as a Parameter in Ruby
"Gem Install Rails" Fails With Dns Error