Rails: Around_* Callbacks

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:

  1. 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).

  2. 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



Leave a reply



Submit