Thursday, 15 April 2021

Rails ActiveRecord Callbacks – PART 2

 Welcome back! I hope, you have finished the first part of this blog post. If not, I recommend you to check it out here because you would get some basic learning about callbacks.

In this blog, you will get an in-depth understanding on below callbacks,

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • before_update
  • around_update
  • after_update
  • after_save
  • before_destroy
  • around_destroy
  • after_destroy
  • after_initialize
  • after_find
  • after_touch
  • Transactional Callbacks (after_commit & after_rollback)
  • Association Callbacks (before_add, after_add, before_remove, & after_remove)

You may be familiar with these callbacks already but I hope, this blog will make you learn something new about these.

before_validation:

  • It’s called before Validations.validate method (which is part of the Base.save call).
  • If this callback throws :abort, the process will be aborted and ActiveRecord::Base#save will return false and nothing will be appended to the errors object.

after_validation:

  • It’s called after Validations.validate method (which is part of the Base.save call).
  • It will run even validation failed so please make sure before using it. (Note: Better to use before_save instead of after_validation.)

Common note for *_validation:

  • The callbacks are before_validation & after_validation.
  • Both will be triggered for the create, update, save, & valid? methods. ( Note: If you want to trigger this callback on specific method, you can use on option.)
  • Note down the below cases when you are using the valid? methodwith on option,

For on: :create,

  • {new_record}.valid? – Callback will be performed.
  • {persisted_record}.valid? – Callback won’t be performed.

For example,

class TestCallback < ApplicationRecord
  after_validation :check_after_validation, on: :create
 
  private
  def check_after_validation
    puts 'Inside after validations.'
  end
end
>> new_record = TestCallback.new(name: 'Test')
>> new_record.valid?
Inside after validations.
true

>> persisted_record = TestCallback.last
>> persisted_record.valid?
true


For on: :update,

  • {new_record}.valid? – Callback won’t be performed.
  • {persisted_record}.valid? – Callback will be performed.

For example,

class TestCallback < ApplicationRecord
  after_validation :check_after_validation, on: :update
 
  private
  def check_after_validation
    puts 'Inside after validations.'
  end
end
>> new_record = TestCallback.new(name: ‘Test')
>> new_record.valid?
true

>> persisted_record = TestCallback.last
>> persisted_record.valid?
Inside after validations.
true


before_save:

  • It’s called before Base.save call.
  • It ignores the on option silently so don’t use on option with before_save callback. (Solution: You can use before_create or before_update instead of an on option. Below example is also a way to overcome this issue.)

For example,

before_save :change_callback_name, :if => :new_record?


Common note for before_* callbacks:

  • The callbacks are before_savebefore_create, and before_update.
  • The validation will be skipped if you did any attribute assignments within these callbacks. All the validations have passed before this callback so if you’re changing any attributes value within these callbacks, the validations won’t run for that. ( Solution: You can use before_validation instead of before_save for attribute assignments with validations)

For example,

class TestCallback < ApplicationRecord
  validates_presence_of :name
  before_save :change_callback_name
 
  private
  def change_callback_name
    self.name = nil
  end
end
>> TestCallback.create(name: 'Test')
(0.4ms)  BEGIN
#<TestCallback id: 18, name: nil, type: nil, created_at: "2020-04-26 04:01:00", updated_at: "2020-04-26 04:01:00">
(3.1ms)  COMMIT
  • The before_* callbacks are normally used to set some extra attributes after validations passed.
  • Note the below points,
    • The database validations won’t be skipped for the before_* callbacks.
    • Don’t perform/enqueue any jobs in these callbacks because the changes won’t be reflected in the database at this point.
    • The updated value should be available within the transaction/same thread.

Common note for *_save callbacks:

  • The callbacks are before_savearound_save, and after_save.
  • These callbacks are triggered for createupdate, & save methods.

Common note for around_* callbacks:

  • The callbacks are around_savearound_create, and around_update.
  • These callbacks are invoked before the life cycle event. If you want to invoke the event itself, you yield to it and then continue execution. 
  • Here is an example for yield,

For example,

def test_yield
  puts "Before yield"
  yield
  puts "After yield"
end
>> test_yield{ puts 'At yield' }
Before yield
At yield
After yield
  • You can use the around callbacks wherever you have both before and after callbacks in the same model for the same event.

For example,

class TestCallback < ApplicationRecord
  before_save :set_callback_name
  after_save :get_callback_name
 
  private
  def set_callback_name
    puts 'set_callback_name'
  end
  def get_callback_name
    puts 'get_callback_name'
  end
end
class TestCallback < ApplicationRecord around_save :set_and_get_callback_name
 
  private
  def set_and_get_callback_name
    puts 'set_callback_name'
    yield
    puts 'get_callback_name'
  end
end

# As you can see here, the output is same for both

>> TestCallback.create(name: ‘test callback')
(0.7ms)  BEGIN
set_callback_name
INSERT INTO "test_callbacks" ……
get_callback_name
(40.0ms)  COMMIT


around_save:

  • Below is an example for around_save and the same example is applicable for other around_* callbacks.

For example,

class TestCallback < ApplicationRecord
  around_save :check_around_save
 
  private
  def check_around_save
    puts 'Inside around save - before yield.'
    yield
    puts 'Inside around save - after yield.'
  end
end
>> TestCallback.create(name: 'Around save callback')
Inside around save - before yield.
(0.4ms)  BEGIN
TestCallback Create (44.7ms)  INSERT INTO ……
Inside around save - after yield.
(46.8ms)  COMMIT
  • If you don’t use yield in around_save callback, It will throw below exception,

ActiveRecord::RecordNotSaved (Failed to save the record)

  • If you have placed multiple yield within this callback, then the action/query will be performed for each yield.

For example,

class TestCallback < ApplicationRecord
  around_save :check_around_save
 
  private
  def check_around_save
    puts 'Inside around save - before yield.'
    yield
    yield
    puts 'Inside around save - after yield.'
  end
end

# As you can see here, an insert query has run on multiple times

>> TestCallback.create(name: 'Around save callback')
Inside around save - before yield.
(0.4ms)  BEGIN
TestCallback Create (44.7ms)  INSERT INTO ……
TestCallback Create (44.7ms)  INSERT INTO ……
Inside around save - after yield.
(46.8ms)  COMMIT


before_create:

  • It’s called before Base.save on new objects that haven’t been saved yet.

Common note for *_create callbacks:

  • The callbacks are before_createaround_create, and after_create.
  • These callbacks are triggered for create and save(for new record only) methods.

around_create:

  • If you don’t use yield in around_create callback, It won’t throw an ActiveRecord::RecordNotSaved exception.
  • An exception would raise from the database, if you have placed multiple yield within this callback.

after_create:

  • It’s called after Base.save on new objects that haven’t been saved yet.
  • if you have any create operation within this callback, then you are in recursion again so make a note of it. (Solution: You have to manually skip the callbacks to fix this issue.)

Common note for after_* callbacks:

  • The callbacks are after_saveafter_create, and after_update.
  • These callbacks are still wrapped in the transaction around save. For example, If you invoke an external indexer within this callback, it won’t see the changes in the database.

before_update:

  • It’s called before Base.save on persisted/existing objects.

Common note for *_update callbacks:

  • The callbacks are before_updatearound_update, and after_update.
  • These callbacks are triggered for create and save(for persisted records only) methods.

around_update:

  • If you didn’t use yield in around_update callback, It won’t throw an ActiveRecord::RecordNotSaved exception.
  • If you have placed the multiple yield within this callback, then the update query will be performed with same params.

after_update:

  • It’s called after Base.save on persisted/existing objects.
  • if you have any update operation within this callback, then you are in recursion again so make a note of it. (Solution: You can use update_columns or manually skip the callbacks.)

after_save:

  • It’s called after Base.save (regardless of whether it’s a create or update save).
  • if you have any create/update operation in this callback, then you are in recursion again so make a note of it.

before_destroy:

  • It’s called before Base.destroy.
  • If you want to destroy or nullify the associated records first, use the dependent: :destroy option on your associations.
  • Sometimes we need the callbacks to execute in a specific order.

For example,
before_destroy callback should be executed before the children records get destroyed by the dependent: :destroy. So you can achieve this by using prepend: true with before_destroy callback. Like below,

For example,

before_destroy :check_before_destroy, prepend: true


If you used prepend: true, the before_destroy gets executed before the dependent: :destroy is called, and the data is still available.

around_destroy:

  • If you didn’t use yield in around_destroy callback, It will throw below exception,

ActiveRecord::RecordNotSaved (Failed to save the record)

  • The multiple delete query won’t run if you have placed multiple yield within this callback.

after_destroy:

  • It’s called after Base.destroy (and all the attributes have been frozen).
  • As I mentioned earlier, If you have another destroy operation within this callback, then it will be an infinite loop. This scenario is applicable for all the callbacks.

after_initialize:

  • It will be called whenever an Active Record object is instantiated, either by directly using new or when a record is loaded from the database.

For example,

class TestCallback < ApplicationRecord
  after_initialize :check_after_initialize
 
  private
  def check_after_initialize
    puts 'Inside after initialize.'
  end
end
>> TestCallback.new(name: 'after_initialize')
Inside after initialize.

>> TestCallback.first
TestCallback Load (1.4ms)  SELECT  "test_callbacks".* FROM "test_callbacks" ORDER BY "test_callbacks"."id" ASC LIMIT $1  [["LIMIT", 1]]
Inside after initialize.
  • It can be useful for avoiding the need to directly override your Active Record initialize method.

after_find:

  • It will be called whenever Active Record loads a record from the database.
  • The after_find callback is performed before the after_initialize callback if both are defined.
  • The after_initialize and after_find callbacks both have the before counterparts.

after_touch:

  • It will be called whenever an Active Record object is touched.
  • It can be used along with belongs_to for updating the parent object’s updated_at whenever update the child object.

Transactional Callbacks:

  • Below are called as transactional callbacks,
    • after_commit
    • after_rollback
  • It will be triggered whenever the database transaction is completed. 

– The after_commit will be trigged if the transaction is successfully committed.

– The after_rollback will be trigged if the transaction is rollback.

  • It’s very similar to the after save except but they don’t execute either database changes has been committed / rollback.
  • We can use after_commit callback only on createupdate, or delete action and below are aliases for those operations.
    • after_create_commit
    • after_update_commit
    • after_destroy_commit
  • You have to rescue it and handle it within the callback in order to allow other callbacks to run.
  • The code executed within after_commit or after_rollback callbacks are not enclosed within a transaction.
  • Note: If you’re using both after_create_commit and after_update_commit in the same model, it will only allow the last callback defined to take effect, and will override all other callbacks. (Solution: Use after_save_commit with on option for create & update.)
  • The after_commit callback triggered for last operation when you did multiple operations for the same instance with in the transaction. Look out the below examples for more understanding,

For example,

class TestCallback < ApplicationRecord
  after_commit :check_after_commit
 
  private
  def check_after_commit
    puts 'Inside after commit.'
  end
end
  • Multiple operations for different instances,
>> ActiveRecord::Base.transaction do
   instance1 = TestCallback.first
   instance1.update(name: ‘after_commit 1')
   instance2 = TestCallback.last
   instance2.update(name: 'after_commit 2')
 end

>> BEGIN
…….
COMMIT
Inside after commit. // Triggered for instance1.update(name: ‘after_commit 1')
Inside after commit. // Triggered for instance2.update(name: 'after_commit 2')
true
  • Multiple operations for the same instances,
>> ActiveRecord::Base.transaction do
   instance1 = TestCallback.first
   instance1.update(name: ‘after_commit 1')
   instance1 = TestCallback.last
   instance1.update(name: 'after_commit 2')
 end

>> BEGIN
…….
COMMIT
Inside after commit. - // Triggered only for instance1.update(name: 'after_commit 2’).
true


Association Callbacks:

The above mentioned callbacks won’t trigger when adding record to an association(collection) using shift left operator(<<) and when removing record form an association using delete method. 

For these actions, we can use association callbacks and perform operations during the callbacks. Below are the available association callbacks:

  • before_add
  • after_add
  • before_remove
  • after_remove
  • before_remove and after_remove callbacks will execute before and after the delete methods. When we call delete method, It will invoke these callbacks by default.
  • before_add and after_add callbacks will execute before and after added association with a record.
  • If the before_add callback throws an exception, it cannot create the association with a record, the record simply won’t be added to the association collection.
  • Similarly If the before_remove callback throws an exception, the record won’t be removed from the association collection.

For example,

has_many :associations, after_add: :check_after_add, after_remove: :check_after_remove
 
def check_after_add
  puts 'Inside after add'
end
 
def check_after_remove
  puts 'Inside after remove'
end


Halting Execution:

  • As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model’s validations, the registered callbacks, and the database operation to be executed.
  • The whole callback chain is wrapped in a transaction. If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued.
  • To intentionally stop a chain use throw :abort.
  • If any callback returns false then callback chain is not halted.

No comments:

Post a Comment