Making Session Data Available to Models in 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

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

# 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.

23 thoughts on “Making Session Data Available to Models in Ruby on Rails”

  1. I’ve done similar things in other architectures. This is useful, but if the container reuses threads (as can happen in J2EE) you can have serious security and other issues.

  2. Alex,
    That’s a very good point. Luckily if you are using Mongrel to deploy your Rails app it creates a new thread to handle each request. If anyone is concerned, then just stick with the ActionController::Caching::Sweeper method mentioned in the article.

  3. Very nice article. It helped me tracking the session user in an audit log which would otherwise have been pretty useless. But how safe is this to use on Apache2 mpm-worker with mod-fcgid? As far as I can see, the before_filter stores the session user in a local thread var on each request in the beginning, so the data stored there should be correct throughout the whole request. Does this hold true on Apache2 mpm-worker with mod-fcgid?

  4. I’ve no idea wether this concern is valid, but will it work with the Evented Mongrel Swiftiply is using? Is it using one thread per request, too?

  5. I got around this in another way. I created a lib in the {RAILS_ROOT}/lib directory that was loaded in config/enviroment.rb. It contains helper functions like is_logged_in?() and get_user_id() that checked for @session and @session[:user] and such, and returned things as needed, these are callable from within the model structure since they are global.

  6. I am reading rails recipes and they mention “Cache Sweepers” as a possible solution for this problem. I am yet to try it out, but you can look at that as an alternative too

    Regards
    Prateek

  7. Is there a simple way to explicitly specify that your app requires mongrel version <= (whatever version is current at the time of writing)? Cause then this is actually not so bad.

  8. Instead of cache_sweeper i use a similar implementation. Just include Auditing to your controller and your observer should inherit the Auditing::Observer. This is a lot cleaner approach than using TLS which might be server/implementation specific.


    module Auditing
    def self.included(base)
    ActiveRecord::Base.observers.each do |observer|
    observer = if observer.respond_to?(:to_sym)
    observer.to_s.camelize.constantize.instance
    elsif observer.respond_to?(:instance)
    observer.instance
    else
    raise ArgumentError, "#{observer} is an invalid class name"
    end
    base.around_filter(observer) if observer.is_a?(Auditing::Observer)
    end
    end

    class Observer < ActiveRecord::Observer
    attr_accessor :controller

    def before(controller)
    self.controller = controller
    end

    def after(controller)
    self.controller = nil
    end
    end

    end

  9. This is very useful information, thanks!

    One thing to consider though is if your model should not be extended to include the information you need in the observer. In the example above that would mean adding an attribute to Account perhaps named last_changed_by holding a reference to the User that last changed the Account. That would then be accessible in the AccountObserver as record.last_changed_by.

  10. Hi, nice post!
    Do you know if this works with FastCGI?
    I tried it with WEBrick and it works, but in production I have FastCGI and I couldn’t confirm it uses one thread per request.
    Thanks!

  11. what about using attr_accessor ?
    for example:


    class Person < ActiveRecord::Base
    attr_accessor :change_user
    #..
    end

    then you can access the data in the controller and assign the value from the session.

  12. This is the best post I’ve found about this issue — a hotly debated topic. Just implemented this for the app I’m working on and we found this solution to be the one we went with. It maintains abstraction while gets the job done.

    One thing to note, in your ApplicationController you don’t need to include UserInfo:

    class ApplicationController < ActionController::Base
    include UserInfo

    The include statement is unnecessary and in fact will get in the way if you have another kind of authentication system (authlogic, restful-authentication) which is going to define a current_user method at the Controller level too.

    So I just took it out there (it is still necessary in the model of course).

    (Since UserInfo.current_user is setting a class variable, all you need is the module loaded, not to mix it into your controller.)

    If anyone sees a problem with this approach let me know.

  13. Thread.current is available to available to the whole app so if user one set Thread.current[:something] to x another user at different location will also get the same variable.
    I am still looking for the perfect method to use sessions in models.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>