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.
user.id
Firstly, don't store the
user object
in the session! Just store the user object's ID. In LoginController#login
, use:
session[:user_id] = user.id
(full method below).
caching helper_method
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
instance variable
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
instance variable
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? %>
security
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
alternatives
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
ApplicationHelper
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!