惟愿终日无丝竹之乱耳,无案牍之劳形。看那平心所至处,皆为仙境。

method_missing神奇魔法之--对付Setting类数据表

上一篇 / 下一篇  2007-08-29 20:27:10 / 个人分类:搞搞技术

查看( 32 ) / 评论( 3 )
原文首发: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,你也可以成为一个超级魔法师哦!

相关阅读:

TAG: method_missing 效率测试

幻 blackanger 发布于2007-08-29 23:03:45
setting  表也不用常读取吧,application.rb中设个初始化方法就行了.

不过这个思路还是挺好的。。。顶一个,性能看起来得到了很大提升啊。。。
幻 blackanger 发布于2007-08-29 23:08:10
感觉像java里的代理。。。不过比java的代理的实现简单多了
SKYOVER之陋室 admin 发布于2007-08-29 23:26:08
个人意见,仅供参考,^_^

效率提升是必然的,因为只读取了一次,假如n=100000,效率就差得更远了,呵呵。
我来说两句

(可选)

Open Toolbar