Jamie Gaskins

Ruby/Rails developer, coffee addict

Better alternative to Rails' before_action to load objects

Published Aug 25, 2014

Rails controllers have a set of callbacks called before_action and after_action (née before_filter and after_filter). They're pretty handy when you want to do something like this:

class AuthenticatedController < ApplicationController
  before_action :require_authentication

  def require_authentication
    # ...
  end
end

class UserProfilesController < AuthenticatedController
  def edit
     # ...
  end
end

In the AuthenticatedController above, we are telling Rails to run the require_authentication method before each action in that controller. Since the UserProfilesController inherits from it, it also inherits that callback.

One thing I see in a lot of code, though, is this:

class ProductsController < ApplicationController
  before_action :find_product, only: [:show, :edit, :update, :destroy]

  def find_product
    @product = Product.find(params[:id])
  end
end

A practice I've been following is never to modify the state of the controller in a callback. Whenever you do, the fact that @product (or whatever instance variable) is set can be surprising the first time you see it because your controller action didn't set it. In large controllers, this can be very difficult to find and if it's set in an inherited controller, it can be downright frustrating.

Another silly thing about using a callback to do this is that you are specifying which actions will be using this instance variable up front. The callback knows which actions are using the objects it loads. If you add a new method that needs that product, you have to add it to the callback list. This is a code smell; the controller itself has direct knowledge of what objects its actions use.

Also, it could be wrong, or at least outdated. For example, if you changed a particular action to load the object a different way than the way it does in the callback (say you want to eager-load some associated objects to avoid N+1 queries), but you forget to remove that action from the callback's list, you're querying the database at least twice for the same record:

class ProductsController < ApplicationController
  before_action :find_product, only: [:show, :edit, :update, :destroy]

  def show
    @product = Product.eager_load(promotions: :discounts).find(params[:id])
  end

  def find_product
    @product = Product.find(params[:id])
  end
end

This isn't necessarily a problem, but it's inefficient because you're querying the database and throwing away the result. It can cause problems, though, if you change how you query the database for that object, say by querying by a slug instead of an id. And then you have to look through the stack trace to try to find out what is firing that query that's broken, but it's a callback, so stack traces are meaningless.

But DRY!

It's important not to repeat the exact same line in every controller action that deals with a singular object:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
  end

  def edit
    @product = Product.find(params[:id])
  end

  def update
    @product = Product.find(params[:id])
    # ...
  end

  def destroy
    @product = Product.find(params[:id])
    # ...
  end
end

Here's what I do instead, and I've found it to work very well:

class ProductsController < ApplicationController
  def show
  end

  def edit
  end

  def update
    if product.update_attributes(params[:product])
      # ...
    end
  end

  def destroy
    product.destroy
  end

  private

  def product
    @product ||= Product.find(params[:id])
  end
  helper_method :product

  def new_product
    @new_product ||= Product.new(params[:product])
  end
  helper_method :new_product
end

Notice there is nothing in the show or edit actions, just as if we were using the callback, but we aren't specifying which methods require that object.

To achieve the best of both worlds, we're using memoization, which is just a fancy word for lazy loading. In order to load the Product into memory, we replace our use the @product instance variable with the @product method (as an aside, using accessor methods in your objects is almost always a good idea over using ivars directly).

Wait, there's already a gem for that

You may have seen this pattern before, but not done quite this way. This is almost exactly what the decent_exposure gem does for Rails controllers, but it gives you a handy DSL to do it:

class ProductsController < ApplicationController
  expose(:product) { Product.find(params[:id]) }
end

Turns out, that behavior is wicked-easy to implement:

class ApplicationController < ActionController::Base
  def self.expose(variable_name)
    define_method(variable_name) do
      @exposed_variables ||= {}
      @exposed_variables[variable_name] ||= yield
    end
    helper_method variable_name
  end
end

That is all it takes. Now you can have a nice DSL for these objects in your controller action or your view without adding yet another gem to your project.

It's up to you and/or your team whether you choose the DSL route or to use explicit methods like the first solution I showed. Either way is a much better solution than loading using a callback. In fact, just don't use callbacks. They're almost never what you want.

TwitterGithubRss