Wrapping Rails Session Hash
Matt Swasey, Former Viget
The best way to use session data in Rails is sometimes a bit elusive. One technique I see from time to time is using a before_filter to pull data from the session, and store it in an instance variable.
class AccountsController < ApplicationController before_filter :find_account ... def find_account @account = Account.find(session[:account_id]) end end
The problem here is that the more places you start using the @account instance variable, the more confusing it gets. Suppose we wanted to reuse this before_filter, and so we move find_account to our ApplicationController. This would further obscure the location where the instance variable is being set.
Another drawback to using a before_filter is having to tediously define where you want the filter to run or not-to-run (using skip_before_filter). It would be nice if we could have that data only where we really needed it without worrying about declaring which actions have access to session data and which don't.
A Better Approach
A pattern I like to use when working with sessions in rails is wrapping session variables in getter and setter current_object methods. I picked this up a while ago while using RestfulAuthentication, I've found since that I use it in almost every project. Observe...
class ApplicationController < ActionController::Base helper_method :current_account def current_account @current_account ||= Account.find_by_id(session[:current_account_id]) end def current_account=(account) session[:current_account_id] = account.try(:id) end end
I am using find_by_id(id) because it returns a nil value when the record is not found rather than an exception, which is what is returned from an unsuccessful find(id). Keep this in mind, as you may prefer to throw an exception over a silent failure.
Using the conditional assignment method in our getter allows us to lazily load database records. It's also worth noting that the current_account method always returns a model instance, we only store the record's id in the actual session. This keeps our session data as small as possible.
I've declared these methods as helper methods inside the ApplicationController so that I can use them in my controllers and views alike. I find a call to 'current_account' in the view is easier to understand than an instance variable that is set somewhere other than the view's corresponding controller action.
Notice the use of Object#try when calling id on the account argument passed to current_account=. Using Object#try in this situation allows us to set session[:current_account_id] to nil using the same current_account= setter method. This is often seen in logout actions:
class SessionsController < ApplicationController ... def destroy self.current_account = nil redirect_to root_path end end
Remember to prepend the setter method with 'self' when using it in your controllers, else Ruby will make a local variable assignment to 'current_account' the variable, which is different from 'current_account=' the method. Obvious, perhaps, but it trips me up from time to time.
class AccountsController def create self.current_account = Account.find(params[:id]) ... end ... end
If anyone has other nice ways of handling session variables in Rails, let me know in the comments!