是非に及ばず

プログラミングに関する話題などを書いていきます(主にRailsやAndroidアプリ開発について)

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すれば良い。