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