【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の色んなところで活躍している
- 例えばDBのカラム名に「created_at」「updated_at」を設定しておくだけで、作成日時と更新日時を自動記録してくれる機能もwith_scopeが一役買っています。