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.

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

Tue, 5 Aug 08 10:03 am . Chris wrote:

I have installed this and it is working perfectly on my development computer. However, I have pushed it out to my test server and it is only updating “updated_by” and not “created_by”. Any ideas on what the problem could be?

Wed, 6 Aug 08 01:45 pm . Herryanto Siatono wrote:

@Chris, I didn’t encounter such problem before, you probably have to track ruby-debug with or println some comments to track what’s going on.

Wed, 6 Aug 08 07:52 pm . Chris wrote:

I think I may have found the issue. It must be difference between MySQL on Windows and MySQL on my Linux server… My created_by field is set to be not null with a default value of 0. On Windows usermonitor works fine and updates both the created_by and updated_by fields. But, when it is executing on Linux it seems like created_by already has the default value of 0 and it fails the condition “&& created_by.nil?”. Shouldn’t this condition be ” && !user.nil?” just like it is in the update_with_user method?

Thu, 7 Aug 08 01:19 pm . Herryanto Siatono wrote:

Chris the !user.nil? for created_by has been checked earlier, while created_by.nil? check is not to auto-assign, if it has been manually assign.

Sat, 9 Jan 10 12:28 am . Brian Candler wrote:

Using a thread-local variable is best, since Rails apps *can* now be run multi-threaded (config.threadsafe!)

A thread-local variable is already used to set Time.zone and have it propagate to the model: see activesupport-2.3.4/lib/active_support/core_ext/time/zones.rb

There are other cases where I’d like the model to interact with the request. For example, on update I’d like to log the IP address of the user (request.ip). Or when doing bulk updates, I’d like them all to have an identical updated_at time; that means taking a ’snapshot’ of the current time for this particular request.

Using thread-local variables works, but somehow it doesn’t feel like a particularly clean way to make the model interact with the request. The logical conclusion would be to set Thread.current[’session’] = session, Thread.current[’request’] = request etc, which is probably going too far.

You also have to be sure that you reset these variables on every request, of course.

Add Your Comment



(optional)