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.