Making Session Data Available to Models in Ruby on Rails

May 29, 2007 - 4 minute read -
ruby-on-rails

Ruby on Rails is implemented as the Model View Controller (MVC) pattern. This pattern separates the context of the Web Application (in the Controller and the View) from the core Model of the application. The Model contains the Domain objects which encapsulate business logic, data retrieval, etc. The View displays information to the user and allows them to provide input to the application. The Controller handles the interactions between the View and the Model.

This separation is a very good design principle that generally helps prevent spaghetti code. Sometimes though the separation might break down.

The following is really an alternative to using the ActionController::Caching::Sweeper which is a hybrid Model/Controller scoped Observer really. It seems to me, based on the name, that the intent is much more specific than giving Observers access to session data. Which do you prefer?

Rails provides the concept of a Model Observer. This Observer allows you to write code that will respond to the lifecycle events of the Model objects. For example you could log information every time a specific kind of Model object is saved. For example you could record some information every time an Account changed using the following Observer:

class AccountObserver < ActiveRecord::Observer
  def after_update(record)
    Audit.audit_change(record.account_id, record.new_balance)
  end
end

You might have noticed a limitation with the previous API though. You didn't notice? The only information passed to the Observer is the Object that is being changed. What if you want more context than this? For example, what if you want to audit not only the values that changed them, but the user who made the change?

class AccountObserver < ActiveRecord::Observer
  def after_update(record)
    Audit.audit_change(current_user, record.account_id, record.new_balance)
  end
end

How do you get the current_user value? Well, you have to plan ahead a little bit. The User in this application is stored in the HTTP Session when the user is authenticated. The session isn't directly available to the Model level (including the Observers) so you have to figure out a way around this. One way to accomplish this is by using a named Thread local variable. Using Mongrel as a web server, each HTTP request is served by its own thread. That means that a variable stored as thread local will be available for the entire processing of a request.

The UserInfo module encapsulates reading and writing the User object from/to the Thread local. This module can then be mixed in with other objects for easy access.

module UserInfo
  def current_user
    Thread.current[:user]
  end</p>
  def self.current_user=(user)
    Thread.current[:user] = user
  end
end

A before_filter set in the ApplicationController will be called before any action is called in any controller. You can take advantage of this to copy a value out of the HTTP session and set it in the Thread local:

class ApplicationController < ActionController::Base
  include UserInfo
  # Pick a unique cookie name to distinguish our session data from others'
  session :session_key => '_app_session_id'
  before_filter :set_user
  protected
  def authenticate
    unless session[:user]
      redirect_to :controller => "login"
      return false
    end
  end</p>
  # Sets the current user into a named Thread location so that it can be accessed
  # by models and observers
  def set_user
    UserInfo.current_user = session[:user]
  end
end

At any point in an Observer of a Model class that you need to have access to those values you can just mixin the helper module and then use its methods to access the data. In this final example we mixin the UserInfo module to our AccountObserver and it will now have access to the current_user method:

class AccountObserver < ActiveRecord::Observer
  include UserInfo
  def after_update(record)
    Audit.audit_change(current_user, record.account_id, record.new_balance)
  end
end

You generally shouldn't need this kind of trick outside of an Observer. In most cases the Controller should pass all of the information needed by a Model object to it through its methods. That will allow the Model objects to interact and the Controller to do the orchestration needed. But in a few special cases, this trick might be handy.