Get more articles like this about using Rails Like A Pro

How should I use ActiveRecord Callbacks?

ActiveRecord callbacks… you’ve probably used them a lot.

Being able to hook into the lifecycle of an object is a powerful feature of Rails and it can give you great gains in productivity early on while you’re building your product.

What happens when the lifecycle of an object gets too tightly coupled with the side-effects?

The danger of this power, like any superpower, is the potential for abuse and what it will do to your objects.

Over time the coupling grows and before you know it, the side effects of creating an instance of one of your objects prevent you from safely instantiating that instance outside the user interface.

Maybe that’s not a problem for you, but I suspect for a lot of people it is. Sending emails is a classic.

Have you ever sent an accidental email to every customer while migrating your data because of an inerrant callback?

---
Hi, 

your account has been charged $29.

Have a good day.
---

Aargh. Cue a whole bunch of concerned customers hitting up your support channels asking what happened.

Or how about testing those little buggers? Those side effects are, at the very least, slowing down your test suite, and at the worst, causing failures. So you end up monkey patching ActiveRecord in order to prevent the callbacks from firing in your test environment.

Is this a code smell?

Callbacks themselves aren’t bad, it’s the way we (ab)use them that is the problem.

Most, perhaps all, side effects shouldn’t be triggered by an ActiveRecord callback. Reserve this precious resource for essential actions that directly affect the object itself.

Email is an example of a side effect that doesn’t belong there.

Sometimes you may need to create other objects as part of the setup process. Again, this is probably a bad idea. Tying the creation of 2 objects so closely together is asking for trouble the moment you need to create one of those objects independent of the other.

class User
  after_create :create_account

  private

  def create_account
    accounts.create
  end
end

What happens when I want to create a User without an Account. I can’t. This kind of dependency is better created outside the model.

So what are they good for?

I find it useful to stick to this simple rule. Only use callbacks on the object they are called on, eg. for a User model, the callbacks should only be able to perform commands on the User instance.

before_save & before_create can be good for guaranteeing default values of your objects. If it’s a constant value, then this is not the right place, that’s definitely a concern better handled by your database.

Calculated values are a different matter. What if you need to create a unique token for each user?

class User
  before_create :set_token

  private

  def set_token
    while User.not.exists?(token: (token = SecureRandom.base64))
      self.token = token
    end
  end

This is where the power of ActiveRecord callbacks shine.

What about the other code that was in my callbacks?

Now that you’re only allowing callbacks to perform commands on the object they’re called on, what do you with the other commands?

I usually approach this in 2 steps. The first step is to identify at what point in the application I need to trigger the callback, and secondly, determine if it is needed in multiple places.

Where code relates to setting up associations on an object the first time it is created, it’s usually safe to move this code into the related controller#action.

Let’s take sending an email as a classic example.

class UsersController < ApplicationController
  def create
    @user = User.create(user_params)
    UserMailer.new(@user).deliver
    respond_with @user
  end
  ...
end

This fits in well here because sending the email is related to creating the user in the context an action taken by a user.

How about creating associations?

When creating or saving an object requires the creation of an association, and this is something we want to do regardless of the context, it’s time to bust it out into another object.

class PersonWithAddress
  def self.create(params)
    new(params).save
  end

  def initialize(params)
    @params = params
  end

  attr_reader :params

  def save
    person.addresses << address
  end

  private

  attr_reader :address, :person

  def person
    @person ||= Person.new(params[:person)
  end

  def address
    @address ||= Address.new(params[:address])
  end
end

This object, given a hash of params containing person and address fields, handles the creation of the person and the address objects and then also the relationship between them.

The intention of the class is clear from the class name, and there are no unexpected side effects. Every time we need a person with an address we just use PersonWithAddress.create(params). And if we just want a person, we simply use Person.create(params).

If we’d used ActiveRecord callbacks to achieve the same thing, we wouldn’t have this clear separation of commands.

tl;dr

  • Limit your use of ActiveRecord callbacks to commands that operate on the object itself.
  • Avoid unexpected side effects in your callbacks.
  • Consider the context of the command to determine whether to use a controller action or another Ruby object.

Get more articles like this about using Rails Like A Pro