Using around_save callbacks in ActiveRecord

It is fairly common to see ‘before’ and ‘after’ callbacks in Rails models to handle instructions for the model to execute prior to and after its state has changed.

However, there might be a need to execute some custom actions in the middle of this callback flow and you might not have control over the callback to make the changes as this might be from say an Engine or a gem.

 1 class B < ActiveRecord::Base
 2   has_many :c
 3   belongs_to :a
 4 end
 5 
 6 class C < ActiveRecord::Base
 7   belongs_to :b
 8 end
 9 
10 
11 # this module could be from a gem or engine
12 module Audit
13   def self.included(klass)
14     klass.extend ClassMethods
15   end
16 
17   module ClassMethods
18     has_many :b
19 
20     after_save :create_b
21 
22     def create_b
23       # saves a record of type B
24     end
25   end
26 end
27 
28 class A < ActiveRecord::Base
29   include Audit
30 
31   # how do we create C which has a reference to B ??
32 end

Take the example above. We have an ActiveRecord model A, whereby after saving it, it creates a record in the database of type B.

The callback is included into A from an Audit module which is external to the application.

How do we trigger a create on C but after the first callback so that we can obtain a reference to B?

The approach I have taken is to use an around_save. I can yield to the main class callback, in this case, creating B, and then execute my custom action to create C afterwards.

The following ruby code demonstrates the idea:

1 def test_yield
2   puts "Hello World!"
3   yield
4   puts "Goodbye World!"
5 end
6 
7 test_yield{ puts "Custom processing here..." } # => Hello World!; Custom processing here...; Goodbye World!

The test_yield method calls the block when it reaches yield and passes control to the code inside the block. Once thats done, control returns to the end of the original method.

I can use an around_save callback which calls yield on the main class to run its callbacks and then have it run my custom code after:

 1 class A < ActiveRecord::Base
 2   include Audit
 3 
 4   around_save :create_c
 5 
 6   def create_c
 7     # calls the after_save defined in Audit module first
 8     yield
 9 
10     # custom call to create C
11     C.create!(b: @b)
12   end
13 end

Happy Hacking!