Railsのnamed_scopeをまとめて実行するサンプル
概要
例えば、こんなモデルがあったとして、
class Member < ActiveRecord::Base belongs_to :customer named_scope by_email, lambda{|email| {:conditions => ['email = ?', email]} } named_scope by_deleted, lambda{|flag| {:conditions => ['deleted = ?', flag]} } end
定義したnamed_scopeをまとめて実行するメソッド(ここではsearch)を作成する事で
以下のように複雑な検索条件に対応しやすくなってコードもスッキリするというお話。
Member.search.find(:all) Member.search(:deleted => false).find(:all) Member.search(:deleted => true, :email => "sample@example.com").paginte(:all, :per_page => 10, :page => params[:page]) customer = Customer.first customer.members.search(:deleted => false).find(:all)
named_scopeをまとめて実行するメソッド
では、本題の検索処理のソースを掲載。
class Member < ActiveRecord::Base def self.search(params = {}) exec_scopes = [] # [実行するスコープ, 引数] を格納する配列 if params[:deleted] == true exec_scopes << [Member.scopes[:by_deleted], true] elsif params[:deleted] == false exec_scopes << [Member.scopes[:by_deleted], false] end if params[:email] exec_scopes << [Member.scopes[:by_email], params[:email]] end # 実行するスコープがない場合 if exec_scopes.size == 0 return Member.scoped end # named_scopeをまとめて実行 exec_scopes.reverse.inject(Member) {|p, s| scope = s.shift args = s if args.size > 0 # 引数がある場合 scope.call p, *args else # 引数がない場合 scope.call p end } end end
重要なポイント
重要なのは、実行するスコープがない場合。つまり、Member.search.find(:all)みたいな呼び出しをされた時にMember.scopedを返す事。これをうっかり忘れると、下記で説明する場合に意図した動きにならないので注意が必要である。
Member.scopedを忘れた場合
class Member < ActiveRecrod::Base def self.search(params = {}) exec_scopes = [] # [実行するスコープ, 引数] を格納する配列 if params[:deleted] == true exec_scopes << [Member.scopes[:by_deleted], true] elsif params[:deleted] == false exec_scopes << [Member.scopes[:by_deleted], false] end if params[:email] exec_scopes << [Member.scopes[:by_email], params[:email]] end # named_scopeをまとめて実行 exec_scopes.reverse.inject(Member) {|p, s| scope = s.shift args = s if args.size > 0 # 引数がある場合 scope.call p, *args else # 引数がない場合 scope.call p end } end end
このような場合、以下のようなコードを書くと
customer = Customer.find(1) customer.members.search.find(:all)
select * from members where customer_id = 1
という感じのSQLが実行されるはずであるが、実際には以下のSQLが実行されてしまう
select * from members
これに対応するために、実行するスコープがない場合(exec_scopes.size == 0)にMember.scopedをreturnすれば良い。