Railsのpaginateでdistinctを扱う

ActiveRecordでdistinctを伴うpaginateをやろうとするとなかなかうまく行きません。
いろいろいじくってやり方を編み出したのでメモ。

まず、ActiveRecordでfindのdistinctを扱うには下記のようにします。

>> RubricksUser.find(:all, :select => 'distinct *')
SELECT distinct * FROM rubricks_users

また、countのdistinctを扱うには下記のようにします。

>> RubricksUser.count(:distinct => true, :select => 'id')
SELECT count(DISTINCT id) AS count_id FROM rubricks_users

さて、paginateでdistinctを使うにはどうすればいいか。
今回やった方法は下記のような感じです。

@pages, @user_list = paginate(:rubricks_user,
  :per_page => 5,
  :select => "distinct *",
  :distinct => true,
  :count    => 'id'
)

paginateでは検索結果を得るためのfindとページ番号を算出するためのcountが両方発行されます。そのため、双方のオプションを重ね合わせるようにパラメータを渡す必要があります。
つまり、:selectはAR.findへ、:distinctはAR.countへ渡されます。なお、:countオプションというのはAR.countの:selectオプションに渡されるものです。

ただし、この方法はそのままでは通らず、エラーが出ます。
paginateの中で
(1):distinctオプションがなぜか不正としてはじかれてしまう

#pagination.rb (distinctがない)
      DEFAULT_OPTIONS = {
        :class_name => nil,
        :singular_name => nil,
        :per_page   => 10,
        :conditions => nil,
        :order_by   => nil,
        :order      => nil,
        :join       => nil,
        :joins      => nil,
        :count      => nil,
        :include    => nil,
        :select     => nil,
        :parameter  => 'page'
      }
...
    def self.validate_options!(collection_id, options, in_action) #:nodoc:
      options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}

      valid_options = DEFAULT_OPTIONS.keys
      valid_options << :actions unless in_action
    
      unknown_option_keys = options.keys - valid_options
      raise ActionController::ActionControllerError,
            "Unknown options: #{unknown_option_keys.join(', ')}" unless
              unknown_option_keys.empty?

      options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
      options[:class_name]  ||= Inflector.camelize(options[:singular_name])
    end

(2)paginateからAR.countに:distinctが渡されない

#pagination.rb
    def count_collection_for_pagination(model, options)
      model.count(:conditions => options[:conditions],
                  :joins => options[:join] || options[:joins],
                  :include => options[:include],
                  :select => options[:count])
    end

という実装になっているためです。

そこで、下記のように強引にpaginate周りをいじる必要があります。

#pluginなどでクラスを再オープンして定義
module ActionController
  module Pagination
    DEFAULT_OPTIONS[:distinct] = false
    
    def count_collection_for_pagination(model, options)
      model.count(:conditions => options[:conditions],
                  :joins => options[:join] || options[:joins],
                  :include => options[:include],
                  :select => options[:count],
                  :distinct => options[:distinct])
    end
    
  end
end

クラス再オープンしている時点で終わってる気がします。もっとスマートなやり方知ってる人は教えてください。
なければRailsにパッチ送ろうかなぁ。

(Shouta)