cult3

Understanding Active Record Callbacks

Nov 25, 2015

Table of contents:

  1. What are Active Record Callbacks?
  2. Registering Active Record Callbacks
  3. Conditional Callbacks
  4. Creating Callback Classes
  5. When should you avoid Callbacks?

As we’ve seen over the last couple of weeks, Active Record objects have a lot of power and responsibility within a Ruby on Rails project.

You will often find that a lot of the business logic of the application is attracted to your Active Record objects.

A common aspect of business logic is that something should happen on a certain action.

For example, whenever an object is created, updated, or destroyed, something should happen as a consequence of that action.

Active Record provides callbacks that allow you to hook into the lifecycle of the object. This means you can take an action whenever one of the lifecycle events occurs.

In today’s tutorial we will be looking at Active Record Callbacks.

What are Active Record Callbacks?

An Active Record callback is basically just a way to trigger logic before or after a change to the object’s state. This allows you to run any arbitrary code automatically whenever that event occurs.

Active Record has a lot of lifecycle events that are triggered around creating, updating, and destroying objects. There are also events that are triggered when using Active Record to query the database.

You can hook onto events that will be run before, during, or after these triggers.

Registering Active Record Callbacks

There are two different ways in which you can register Active Record Callbacks.

Firstly, you can define a method to be called:

class Identity < ActiveRecord::Base
  before_create :strip_spaces_from_serial

  protected

  def strip_spaces_from_serial
    serial = serial.strip
  end
end

Secondly, you can use the macro style:

class Identity < ActiveRecord::Base
  before_create { serial = serial.strip }
end

Whilst you are free to use either method, it’s usually better to only use the macro style if what you are doing is really simple.

You can also register callbacks to only be fired on certain triggers:

class User < ActiveRecord::Base
  before_validation :email_to_downcase, on: :create

  protected

  def email_to_downcase
    email = email.downcase
  end
end

In this example the email_to_downcase callback will only be run on the create action.

Conditional Callbacks

Sometimes it might be desirable to only run callbacks if a certain condition is satisfied. This can be achieved by using the :if and :unless options:

class Folder < ActiveRecord::Base
  before_save :set_folder_position, if: :folder_has_moved?
end

In the example above, the callback will only be execute if the folder_has_moved? method returns true. If you would like the inverse of this logic, you can use the :unless option.

Alternatively you can also pass a Proc (What are Ruby Procs?) to the :if and :unless options:

class Book < ActiveRecord::Base
  before_save :strip_identifier,
              if: Proc.new { |book| book.identifier_changed? }
end

Again, as with the macro style registration of callbacks, it’s up to your discretion as to which version you use, but it’s probably better to only use a Proc if the check is really short and concise.

Creating Callback Classes

Sometimes you might want to extract a callback into it’s own class for reuse or for isolation. For example, perhaps whenever a new user is created we want to set a timestamp to expire their trial for the application:

class User < ActiveRecord::Base
  after_create SetTrialExpiresAt
end

You can then deal with the callback in it’s own class:

class SetTrialExpiresAt
  def self.after_create(user)
    # Set the timestamp
  end
end

By abstracting the callback into it’s own class it also makes it really easy to reuse callbacks across different models.

When should you avoid Callbacks?

Callbacks can be great as a way to run code on certain actions of the Active Record lifecycle.

However, Callbacks can also turn into a nightmare!

Callback logic can often get really complicated as more and more callbacks are added to the model.

Callbacks are also not always guaranteed to run. If a developer implements a certain bit of functionality that skips the callback for whatever reason, important business logic might be circumvented.

The scope of what should happen as a consequence of a callback should also be thought about a lot. Whilst callbacks are good for certain actions such as modifying data on the object, it’s probably not a good idea to start using them to automatically send emails, or add the user to an external service! There are much better ways to deal with these types of events than shoehorning it into the model.

I would try and keep your usage of callbacks to a minimum, make it obvious as to what will happen on certain actions and don’t let yourself get sucked into callback hell.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.