Rails logged_in_user idiom
I recently refactored another developer's code to clean up some massive amounts of database pings to retrieve the current user object. The main problem was in the use of the logged_in_user or current_user idiom in Rails. Several places give good examples but leave out the details that were bugging my project.
Firstly, don't store the
in the session! Just store the user object's ID. In
session[:user_id] = user.id
(full method below).
Rather than a before filter on ApplicationController, I used a helper method logged_in_user:
class ApplicationController < ActionController::Base helper_method :logged_in_user private def logged_in_user return (@logged_in_user=nil) if session[:user_id].nil? @logged_in_user ||= User.find_by_id(session[:user_id]) end def reset_logged_in_user @logged_in_user=nil end #... end
The method returns the
of the same name (
@logged_in_user). If the variable is not already set, only
then does the method use find_by_id to load the object from the database
(saving an extra database ping over other examples). An
is scoped to the instance of
ApplicationController, i.e. it exists for the duration of the web request.
Thus it acts as a cache during the request. We can then call logged_in_user
from our views without repeatedly accessing the database:
<% if logged_in_user %>
and we can access any User methods like this:
<% if logged_in_user and logged_in_user.may_view_all? %>
For security, be sure your LoginController resets both the user ID in the session and the logged_in_user instance variable:
class LoginController < ApplicationController #... def login session[:user_id] = nil reset_logged_in_user if request.post? user = User.authenticate(params[:username], params[:password]) if user session[:user_id] = user.id redirect_to(:action => "index") else flash[:notice] = "Invalid user/password combination" params[:password]=nil end end end def logout session[:user_id] = nil reset_logged_in_user reset_session flash[:notice] = "Logged out" redirect_to(:action => "login") end end
Here are two other ways of writing logged_in_user that show the logic a little more clearly, if more verbose:
def logged_in_user @logged_in_user = User.find_by_id(session[:user_id]) if @logged_in_user.nil? and session[:user_id] @logged_in_user end def logged_in_user if session[:user_id].nil? @logged_in_user=nil elsif @logged_in_user.nil? @logged_in_user = User.find_by_id(session[:user_id]) end @logged_in_user end
Final tip: make sure you don't put a logged_in_user helper method on ApplicationHelper--- that's how our app was pinging the database for the user object tens of times per page view as this method was getting used instead of ApplicationController's!