will_paginateでのDISTINCT指定など

railsでwill_paginate使う場合で結果をdistictしようとしてすこしはまったのでメモです。

selectオプションにDISTINCTを指定

まず、単純にpaginateメソッドのselectオプションに「DISTINCT」を指定してみました。

    self.paginate(:all,
     :joins => joins,
     :conditions => criteria.to_where_clause,
     :order => order,
     :select => " DISTINCT products.* ",
     :page=> page,
     :per_page => per_page)    

こうするとMysqlのSyntaxエラーが発生しました
ログでSQLを確認してみると以下のようになっていました。

SELECT count( DISTINCT products.* ) AS count_distinct_products_all FROM `products` ..省略..

(当然、こういう結果になるか!!)
「count( DISTINCT products.* )」ていうところが不正で「count( DISTINCT products.id )」とかにしないといけないのですね。

countオプションを指定

で,will_paginateのソースなどを調べたりして、以下のようにしたら「count( DISTINCT products.id )」のように選択カラムを選択できるようになりました。

    self.paginate(:all,
     :joins => joins,
     :conditions => criteria.to_where_clause,
     :order => order,
     :select => " DISTINCT products.* ",
     :count    => { :select => "DISTINCT id"},
     :page=> page,
     :per_page => per_page)    

:countオプションとしてcountのsqlを発行するさいの(ActiveRecord::Baseのcountメソッドで指定するものと同様の)オプションをハッシュで指定できるということでした。

will_paginateのソース確認

will_paginateでcountを取得しているのはの以下に引用しているfinder.rbのWillPaginate::Finder::ClassMethod.wp_count!というメソッドです。
「count_options = upate(options.delete(:count)」ではじまる行がpaginateへの:countのハッシュの内容をcountメソッドにわたすoptionに追加しているところですね。
:countのハッシュで指定すればそちらがcountメソッドのオプションとなり、指定しなかった場合はpaginateでわたしたオプションがそのままつかわれるということですね。
(ただし、:order,:limit,:offsetなどのオプションは省かれます。)

def wp_count!(options, args, finder)
  excludees = [:count, :order, :limit, :offset]
  unless options[:select] and options[:select] =~ /^\s*DISTINCT/i
    excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
  end
  # count expects (almost) the same options as find
  count_options = options.except *excludees
  # merge the hash found in :count
  # this allows you to specify :select, :order, or anything else just for the count query
  count_options.update(options.delete(:count) || {}) if options.key? :count

  # we may have to scope ...
  counter = Proc.new { count(count_options) }

  # we may be in a model or an association proxy!
  klass = (@owner and @reflection) ? @reflection.klass : self

  count = if finder =~ /^find_/ and klass.respond_to?(scoper = finder.sub(/^find_/, 'with_'))
            # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
            # then execute the count with the scoping provided by the with_finder
            send(scoper, &counter)
          elsif conditions = wp_extract_finder_conditions(finder, args)
            # extracted the conditions from calls like "paginate_by_foo_and_bar"
            with_scope(:find => { :conditions => conditions }, &counter)
          else
            counter.call
          end

  count.respond_to?(:length) ? count.length : count
end