Search
⌘K
    to navigateEnterto select Escto close

    Active Record callbacks and object life cycle

    When a blog is created, then we might want to generate a permanent URL for the blog. Similarly, when someone changes one's password, then we might want to send an email to the person that the password has been changed.

    We can achieve such business cases by registering some methods that should be executed by Active Record whenever a model changes.

    List of callbacks

    Active Record Callbacks are hooks to which we can register methods in our models. These hooks are executed in various stages of an Active Record object lifecycle. The following callbacks are run when an object is created. These callbacks are executed in the order in which they are listed below.

    1before_validation
    2after_validation
    3before_save
    4around_save
    5before_create
    6around_create
    7after_create
    8after_save
    9after_commit/after_rollback
    

    The before_create, around_create and after_create are replaced by before_update, around_update and after_update respectively when records are updated.

    Active Record provides only four callbacks when records are destroyed. They are invoked in the following order.

    1before_destroy
    2around_destroy
    3after_destroy
    4after_commit/after_rollback
    

    Active Record callbacks are useful as we can place repetitive behavior in a single place, in turn abiding to the DRY principle.

    Registering callbacks

    Using callbacks for validations

    In the previous chapter, we had introduced a validation to ensure that task has a title. Let's try and modify the value of the title attribute through callbacks and observe the behavior.

    Open task.rb file and add following lines of code:

    1class Task < ApplicationRecord
    2  # previous code
    3
    4  before_validation :set_title
    5  validates :title, presence: true, length: { maximum: 50 }
    6
    7  private 
    8
    9    def set_title
    10      self.title = "Pay electricity bill"
    11    end
    12end

    Now open Rails console and execute the following code.

    1$ bundle exec rails console
    2>> task = Task.new
    3>> task.valid?
    4#=> true
    

    As seen above, task.valid? returned true, which means the title was set before the validations were run. Let's verify if the value was actually set on the record.

    1>> task.title
    2#=> "Pay electricity bill"
    

    Yay! The task record has title set by the callback. Now comment out the line where callback is registered, and register the same method in an after_validation callback:

    1# before_validation :set_title
    2after_validation :set_title

    Open Rails console and check the following:

    1>> task = Task.new
    2>> task.title
    3#=> nil
    4
    5>> task.valid?
    6#=> false
    7
    8>> task.title
    9#=> "Pay electricity bill"
    

    Interesting isn't it! The validation has failed and returned false, but we still have a title assigned. That's because the attribute was assigned after the validations were run.

    As you would expect, task.save! should fail here:

    1task.save!
    2#=> ActiveRecord::RecordInvalid: Validation failed: Title enter a value for Title
    

    Using callbacks for saving data

    Let's modify the Task model to enable before_validation again:

    1before_validation :set_title
    2
    3# Comment out after_validation callback
    4# after_validation :set_title

    Let's add a before_save callback to change the title. Let's also ensure that we also have our before_validation callback that was enabled above as well:

    1class Task < ApplicationRecord
    2  # ... existing validations and callbacks
    3
    4  before_save :change_title
    5
    6  def change_title
    7    self.title = "Pay electricity & TV bill"
    8  end
    9
    10end

    Time to try things using rails console:

    1>> task = Task.new
    2>> task.valid?
    3#=> true
    4
    5>> task.title
    6#=> "Pay electricity bill"
    7
    8>> task.save!
    9>> task.title
    10#=> "Pay electricity & TV bill"
    

    Let's look at the value stored in the database:

    1>> task = Task.order(created_at: :desc).first
    2>> task.title
    3#=> "Pay electricity & TV bill"
    

    As we can see, the value stored in the database is what we have set from before_save callback. We can observe similar results if we use before_create.

    Callbacks like after_create, after_save and after_commit will have a different behaviour in above scenarios. Comment out existing before_save callback and enable after_save for same methods:

    1# before_save :change_title
    2after_save :change_title

    Observe results in Rails console:

    1>> task = Task.create!
    2>> task.title
    3#=> "Pay electricity & TV bill"
    4
    5>> task = Task.last
    6>> task.title
    7#=> "Pay electricity"
    

    The task object returned from Task.create! has title updated from the after_save callback. But the value in the database is the one that was set from our before_validation callback.

    after_save vs after_commit

    There is a slight but important difference between after_save and after_commit callbacks. after_commit is invoked when a database transaction reaches committed state. In case a transaction fails, for instance, while creating a record, after_commit is not invoked by Active Record, but after_save will have run by then. If there is a requirement that necessitates data to be persisted in the database before executing a piece of code, we should use after_commit instead of after_save.

    Triggering multiple callbacks of the same type

    We can register multiple methods for a callback. They will be chained up and executed in the same order in which they are registered. Let's have two before_validation callbacks in our Task model:

    1class Task < ApplicationRecord
    2  # previous code
    3
    4  before_validation :set_title
    5  before_validation :print_set_title
    6
    7  private
    8
    9    def set_title
    10      self.title = 'Pay electricity bill'
    11    end
    12
    13    def print_set_title
    14      puts self.title
    15    end
    16end
    17
    

    Reload Rails console and observe the following:

    1>> task = Task.new
    2>> task.title
    3#=> nil
    4
    5>> task.valid?
    6#=> "Pay electricity bill"
    

    When valid? method is called, set_title is executed first and Pay electricity bill is assigned to the title attribute. Then print_assigned_title is executed and the assigned value is printed.

    Conditionally triggering callbacks

    Let's modify our before_validation callback as follows:

    1before_validation :set_title, if: :title_not_present
    2
    3def title_not_present
    4  self.title.blank?
    5end
    6
    7def set_title
    8  self.title = 'Pay electricity bill'
    9end

    Here we make sure that the title is assigned a value only when title_not_present method returns true. Let's verify this behavior in Rails console:

    1>> task = Task.new(title: "This is a sample task")
    2>> task.valid?
    3#=> true
    4
    5>> task.title
    6#=> "This is a sample task"
    

    As we can see, title is not set from the before_validation callback. Similarly, we can use unless: to conditionally trigger a callback when a negative condition is provided:

    1before_validation :assign_title, unless: :title_present
    2
    3def title_present
    4  self.title.present?
    5end

    Understanding how the callbacks are triggered

    The following methods, when called on an Active Record object, trigger the above-described callbacks:

    1create
    2create!
    3destroy
    4destroy!
    5destroy_all
    6save
    7save!
    8save(validate: false)
    9toggle!
    10touch
    11update_attribute
    12update
    13update!
    14valid?
    

    save(validate: false) runs all callbacks as that of save, except validate callbacks.

    Callbacks are NOT triggered when the following methods are called:

    1decrement
    2decrement_counter
    3delete
    4delete_all
    5increment
    6increment_counter
    7toggle
    8update_column
    9update_columns
    10update_all
    11update_counters
    

    Except the title presence validation we don't need anything else to be committed in this chapter:

    1git clean -fd
    Previous
    Next