If your tables have created_at/created_on and updated_at/updated_on columns, Ruby On Rails (RoR) framework is smart enough to auto-assign the date/time value, without you writing a single line of code.

That’s great. But when you need to keep track of created_by and updated_by, you have to manually update your controllers to set the values. This obviously contradicts with Don’t Repeat Yourself (DRY) principle of Ruby. So, as a good and obedient Ruby programmer, let’s work around that.

For a start, RoR wiki is the best place to find information, it provides a write up on how to enhance ActiveRecord::Base to auto update created_by and updated_by. The tricky part is how do you retrieve the currently logged-in user, which is usually only exposed in controller and view layers, but not the data access layer.

If you have read the article, it shows a clever way by adding a class method (current_user) to user.rb, and updating aplication.rb to set currently logged-in user:

user.rb

cattr_accessor :current_user

application.rb

before_filter do |c|
    User.current_user = User.find(c.session[:user]) unless c.session[:user].nil?
end

This way however is prone to concurrency issue, because RoR Dispatcher is not thread-safe and class variable is a single instance variable. Thus concurrent thread executions may provides current_user value with the other concurrently-executing-logged-in user.

Another way is to use Thread.current. With it, you can store a short-live variable value for the executing thread, and it is thread-save.

Now, you only need to update the application.rb and leave user.rb intact. Copy usermonitor.rb in to the /lib directory and you are done, a RoR framework that auto-updates created_by and updated_by, isn’t RoR wonderful?

application.rb

class ApplicationController < ActionController::Base
  ....

  before_filter :set_current_user
  
  def set_current_user
    Thread.current['user'] = session[:user]
  end
end

usermonitor.rb

module ActiveRecord
  
  module UserMonitor
    def self.included(base)
      base.class_eval do
        alias_method_chain :create, :user
        alias_method_chain :update, :user

        def current_user
          Thread.current['user']
        end
      end
    end

    def create_with_user
      user = current_user
      if !user.nil?
        self[:created_by] = user.id if respond_to?(:created_by) && created_by.nil?
        self[:updated_by] = user.id if respond_to?(:updated_by)
      end
      create_without_user
    end
    
    def update_with_user
      user = current_user
      self[:updated_by] = user.id if respond_to?(:updated_by) && !user.nil?
      update_without_user
    end
    
    def created_by
      begin
        current_user.class.find(self[:created_by]) if current_user
      rescue ActiveRecord::RecordNotFound
        nil
      end
    end
   
    def updated_by
      begin
        current_user.class.find(self[:updated_by]) if current_user
      rescue ActiveRecord::RecordNotFound
        nil
      end
    end
  end
end

Please note the following changes to usermonitor.rb as compared with the version found in the wiki:

  • Use alias_method_chain for cleaner code, it overrides ActiveRecord::Base#create to call create_with_user, and link the previous ActiveRecord::Base#create method with create_without_user.
    alias_method_chaing is currently only available for for RoR on Edge
  • Overriding ActiveRecord::Base is no longer necessary due to auto-retrieval of model name through current_user.class, which assumed to return the user object instead of just the user id.

Finally, remember to add the following codes to extend ActiveRecord::Base with UserMonitor:
config/environment.rb

require 'usermonitor'
ActiveRecord::Base.class_eval do
    include ActiveRecord::UserMonitor
end

UPDATE: After delving with RoR for a while, I found out that Rails is based on CGI request model, thus there is not thread safety issue, and you don’t necessarily need to use Thread.current to store your current user, and an instance variable is as good.

13 Comments . Comments Feed . Trackback URI
Tue, 15 Aug 06 04:45 pm . Programming » (Distributed object programming) Rails - Auto Assign Created By and Updated By wrote:

[…] Rails - Auto Assign Created By and Updated ByIf your tables have created_at/created_on and updated_at/updated_on columns, Ruby On Rails (RoR) framework is smart enough to auto-assign the… […]

Sun, 8 Oct 06 07:58 pm . Eduardo Flores Verduzco wrote:

Great! I\’ll use a similar technique to implement localization of database text fields… (using gettext you can localize the code-embedded text but not the text fields, the idea is to get access to a session[:lang] variable to identify current user\’s chosen language in a similar way you are getting access to the user\’ data)

Mon, 9 Oct 06 08:37 am . Herryanto Siatono wrote:

Yep, that sounds like a good idea.

Fri, 13 Oct 06 03:58 am . Phillip Hershkowitz wrote:

Hi Herryanto,

I installed login_generator to add authentication to my project (http://wiki.rubyonrails.com/rails/pages/LoginGenerator). After I installed the auto assign solution my user unit tests started to fail, complaining of a nil user. Have you heard of this problem?
Thanks,
Phil

Mon, 16 Oct 06 08:58 am . Herryanto Siatono wrote:

I’m really sorry Phil, haven’t heard of such problem before. Think you may have to start pumping out some variable values, to see which part went wrong.

Mon, 16 Oct 06 07:07 pm . Steven Hann wrote:

Hi, I’m trying to build a photosharing gallery for my friends and I, anyway I’m trying to implement the “created_by” functionality.

I’ve followed your instructions, so now I have “created_by” columns in my table, usermonitor.rb, the thread.current code implemented but when I run my create method it doesn’t automatically update the columns. I’m not quite sure what I’m doing wrong.

Currently the code sits like this in my

pictures_controller.rb

def create
@picture = Picture.create! params[:picture]
@picture.tag_with(params[:tag_list])
redirect_to :action => ’show’, :id => @picture
rescue ActiveRecord::RecordInvalid
render :action => ‘new’
end

with params[:picture] taking in, title, description and the file itself.

I think the problem is that I’m not referencing the current user

Thank you very much for any direction you could give me!

Mon, 16 Oct 06 09:56 pm . Herryanto Siatono wrote:

Steven, I’ve just added the instruction to to extend ActiveRecord::Base with UserMonitor. You may have missed that out.

Tue, 28 Nov 06 10:27 am . Vidal Graupera wrote:

This is helpful. Thanks. However, something is not quite right with this code. Typically, session[:user] is an integer ID, so you have current_user.class.find where this is a Fixnum and not User object.

Sat, 23 Dec 06 02:01 am . Herryanto Siatono wrote:

It really depends, Vidal, feel free some people just store id in session[:user] while some store the user object.

Feel free to tweak the codes around to meet your needs. :)

Sat, 14 Apr 07 02:24 am . Platte Daddy wrote:

Did you know that Rails keeps each request isolated? It’s built on a CGI model that chucks everything after each request. You don’t have to worry about thread safety per request.

The only time you would need to worry about thread safety in Rails is if you’re working with threaded code of your own.

Sat, 14 Apr 07 09:34 am . Herryanto Siatono wrote:

@Platte Daddy, yeah I realized that after I delve more into rails, was a newbie back then. Thanks for the update. I’ve put up a note in my post.

Wed, 15 Aug 07 07:54 am . Daryl wrote:

Despite your thread safety, is this method still okay to use?
Is it overkill or is there no impact over the traditional method?
Do you still use this method? =)

Wed, 15 Aug 07 09:37 am . Herryanto Siatono wrote:

@Daryl, there’s no thread safety issue, yep it’s okay to use. Yeah I would say no impact. I’ve been using it since day one, no issue at all.

Add Your Comment



(optional)