Better alternative to Rails' before_action to load objects
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.