【rails】alias_method_chainとwith_scopeに関して

Railsアプリケーションを作っていく時に使える、alias_method_chainとwith_scopeのスマートな合わせ技をご紹介します。
Railsは1系を利用しています。2系からはnamed_scopeが使えるので、そちらを使うべきかと。

データ構造
create_table :users do |t|
  t.column :name, :string
  t.column :status, :enum, :limit => [:active, :inactive], :default => :active
  t.column :age, :tinyint
  t.column :company_id, :integer
  t.column :created_at, :timestamp
  t.column :updated_at, :timestamp
end
create_table :companies do |t|
  t.column :name, :string
  t.column :created_at, :timestamp
  t.column :updated_at, :timestamp
end
  • ユーザー(users)テーブルには論理削除機能をつけるためにstatusカラムが存在している
  • user has_many companies


状況設定
  • ユーザーをfindする時は、基本的にststus => :activeなもの
  • ユーザーをカウントする時も、基本的にstatus => :activeなもの


毎回find,countする時に条件書くの面倒くさいな…
  • そんなときに今回ご紹介するalias_method_chainとwith_scopeを使います
class  User < ActiveRecord::Base
  has_many :companies

  class << self
    def find_with_active(*args)
      with_scope(:find => {:conditions =>["status = ?", :active]}) do
        find_without_active(*args)
      end
    end
    alias_method_chain :find, :active

    def count_with_active(*args)
      with_scope(:find=> {:conditions =>["status = ?", :active]}) do
        count_without_active(*args)
      end
    end 
    alias_method_chain :count, :active
  end

  def destroy
    update_attribute(:status, :inactive)
  end
end
  • alias_method_chain :find, :activeは、以下の宣言と同義です。
alias_method :find, :find_with_active
alias_method :find_without_active, :find  
  • すなわち、findメソッドでfind_with_activeメソッドが呼び出され、find_without_activeメソッドでfindメソッドが呼び出されることになります。
  • この2行をたった1行で表現してくれるのがalias_method_chain :find, :active


上のコードをもう少しスマートに書く
class << self
  def find_with_active(*args)
    actived_scope{find_without_active(*args)}
  end
  alias_method_chain :find, :active
 
  def count_with_active(*args)
    actived_scope{count_without_active(*args)}
  end
  alias_method_chain :count, :active
        
  def actived_scope
    scope_condition = {:conditions => ["working_hours.status = ?", :active]}
    
    with_scope(:find => scope_condition) do
      yield    # スマートポイント!
    end
  end
end  



ん?findでcondition指定しているのに、countって適用されるの?
  • それが適用されるのです!詳しくはソース見ると分かります。
def with_scope(method_scoping = {}, action = :merge, &block)
  method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)

  # Dup first and second level of hash (method and params).
  method_scoping = method_scoping.inject({}) do |hash, (method, params)| 
    hash[method] = (params == true) ? params : params.dup
    hash
  end

  method_scoping.assert_valid_keys([ :find, :create ])

  if f = method_scoping[:find]
    f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :order, :readonly, :lock ]) #ココが重要!
    f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly)
  end

  # Merge scopings
  if action == :merge && current_scoped_methods
    method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
      case hash[method]
        when Hash
          if method == :find
            (hash[method].keys + params.keys).uniq.each do |key|
              merge = hash[method][key] && params[key] # merge if both scopes have the same key
              if key == :conditions && merge
                hash[method][key] = [params[key], hash[method][key]].collect{ |sql| "( %s )" % sanitize_sql(sql) }.join(" AND ")
              elsif key == :include && merge
                hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
              else
                hash[method][key] = hash[method][key] || params[key]
              end
            end
          else
            hash[method] = params.merge(hash[method])
          end
        else
          hash[method] = params
      end
      hash
    end
  end
                   
  self.scoped_methods << method_scoping
 
  begin 
    yield
  ensure    
    self.scoped_methods.pop
  end
end
  • 要はcountメソッドに{:conditions => ["users.status = ?", :active]}が追加されただけなんです。
  • 「ココが重要!」と書いた行にあるオプションに対して条件を付けることができます。
  • orderなんかも使い所多そうですね。defaultで降順に並び変えたい時とか。
  • あとはうまくやればページネーションも実装できるかも。今度試してみます。


with_scopeはActiveRecordの色んなところで活躍している


結論
  • with_scopeもalias_method_chainもとっても便利
  • ActiveRecordを学習するのはとっても楽しい!これからも学習を続けましょうということです。