原文首发:Ruby中文社区
社区地址:http://www.ruby-lang.org.cn/
转载请保留版权声明并以连接形式指向原文。在我们开发网站的过程中,可能经常会有一张Settings 或者 config 的表,这些表有如下特征:
1. 含有 id, name, value三个字段
2. name唯一
3. 表内的行数一般为固定行,不会插入新的纪录。
4. 大部分操作为读取,很少修改
面对这样的表,我们会有一个模型:
CODE:
class Setting < ActiveRecord::Base
validates_presence_of :name
validates_format_of :name, :with => /([a-z]{1})[0-9a-zA-Z_]+/i, :message => 'must be a valid name.'
end因为这样的表经常需要读取,而
Rails常用的方法为:Setting.find_by_name('site_name') 这样的操作每次都要去数据库中去取,造成不必要的效率浪费,而且每次都要使用一个find_by_name的方法使用也不太方便,有没有一种办法能够更方法的使用这类型的表呢?有!答案就在于神奇的
method_missing.
我们需要做的就是将Setting.find_by_name(:attr_name)这样的实例方法变成Setting.attr_name这样的读取方式,看上去就舒服多了。
看看我们的实现吧。
CODE:
def self.method_missing(method_id, *arguments)
if arguments.length == 0
#try to get from a static variable, if not exists, then return a nil as value
val = eval("@@#{method_id.to_s} ||= nil")
if val.nil?
#if this variable is nil, then we try to get it from database
s = Setting.find_by_name(method_id.to_s)
#if there isn't a record called this name, then we raise a NoMethodError
raise(NoMethodError, method_id, caller) if s.nil?
#cache it to the static vairable.
eval("@@#{method_id.to_s} = '#{val = s.value}'")
end
val
elsif method_id.to_s[-1,1] == '='
#remove '=' to get the real attribute name.
method_name = method_id.to_s.sub(/=/, '')
#update the attribute as value.
Setting.find_by_name(method_name).update_attribute(:value, arguments[0])
#update the static variable.
eval("@@#{method_name} = '#{arguments[0]}'")
else
super
end
end代码很简单,而且都有注释,我们就不详细的说明了,稍微说下流程。
def self.xxx,就是这个method_missing是针对Setting这个类的静态方法,而不是实例方法。
如果我们检查到方法名称没有带参数传入,则首先尝试取得内存中的缓存值,如果为空,则说明没有缓存或者方法不存在,再试图去数据库中找到name为这个方法名称的行,如果没有则抛出错误。
正常从数据库中取回值之后,就将这个值缓存入类变量中,以备下次使用,然后返回这个值。
如果我们检测到方法名称中含有等号,则证明这是一个赋值方法,把等号去掉,得到真正的字段名称,然后使用update_attribute来更新到数据库中,再更新缓存中的值。
如果不符合上述两种情况,则转交给上级处理。
好了,现在我们就直接可以使用了。
CODE:
Setting.site_name
#=> 'My Site'
Setting.site_name = 'Hello Ruby Chinese Community!'
#=>'Hello Ruby Chinese Community!'
Setting.site_name
#=>'Hello Ruby Chinese Community!'看上去没有问题,为了安全起见,我们用
单元测试来测试一下我们的劳动成果。
setings.yml
CODE:
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
site_name:
id: 1
name: 'site_name'
value: 'My Site'
site_url:
id: 2
name: 'site_url'
value: 'http://ruby-lang.org.cn/'setting_test.rb
CODE:
require File.dirname(__FILE__) + '/../test_helper'
class SettingTest < Test::Unit::TestCase
fixtures :settings
# Replace this with your real tests.
def test_select
assert_equal 2, Setting.count
end
def test_create
setting = Setting.new(:name => 'enable_news', :value => 1)
assert setting.save
assert_equal 1, setting.value_before_type_cast
end
def test_validate
setting = Setting.new(:name => '#$#$##%')
assert !setting.save
assert_equal 1, setting.errors.size
assert_equal 'must be a valid name.', setting.errors.on(:name)
end
def test_static_attributes
assert_equal 'My Site', Setting.site_name
assert_equal 'http://ruby-lang.org.cn/', Setting.site_url
Setting.site_name = 'My WebSite'
assert_equal 'My WebSite', Setting.site_name
setting = Setting.new(:name => 'enable_news', :value => 1)
assert setting.save
assert '1', Setting.enable_news
end
end嗯,运行测试,OK! 我看到了绿色的状态条。(我使用Apatan)。^_^
怎么样,这样就方便多了吧!而且最重要的是这样的读取性能提高了很多,我们来测试一下性能如何。
CODE:
require 'benchmark'
include Benchmark
n = 1000
bm(12) do |x|
x.report('static get'){ n.times { Setting.site_name }}
x.report('dynamic get'){ n.times { Setting.find_by_name('site_name') }}
x.report('static set'){ n.times { Setting.site_name = 'test' }}
x.report('dynamic set'){ n.times { Setting.find_by_name('site_name').update_attribute('value', 'test') }}
end输出:
CODE:
user system total real
static get 0.020000 0.000000 0.020000 ( 0.052925)
dynamic get 0.770000 0.040000 0.810000 ( 0.845212)
static set 2.220000 0.150000 2.370000 ( 2.680938)
dynamic set 2.090000 0.190000 2.280000 ( 2.760397)从这里我们可以看到,读取的性能差了好多个数量级,而更新的时候因为多了一步eval,所以效率稍微有一点儿差异,不过我们的上的本身就是读取,对吧?
怎么样,了解了神奇的method_missing,你也可以成为一个超级魔法师哦!