Railsメモ - paginateを任意のSQLで

Railsにはpaginateという便利なメソッドがあって、googleみたいにたくさんの検索結果を
複数ページにわけるのが簡単にできる。(Previous, Nextとかページの最後に表示されるやつ。)
こちらのページがわかりやすい。

問題は、自前でSQL文を書いてfind_by_sqlでデータを引っ張ってきているときだ。
paginageはrails備え付けのfindメソッドを使っているとき用に書かれているので、
SQL文の生書きをしているときは、そのままでは使えない。

railsのソース rails/actionpack/lib/action_controller/pagination.rbをみると、
paginateのソースは以下のようになっている。

pagination.rb

    def paginate(collection_id, options={})
      Pagination.validate_options!(collection_id, options, true)
      paginator_and_collection_for(collection_id, options)
    end

    def paginator_and_collection_for(collection_id, options) #:nodoc:
      klass = options[:class_name].constantize
      page  = params[options[:parameter]]
      count = count_collection_for_pagination(klass, options)
      paginator = Paginator.new(self, count, options[:per_page], page)
      collection = find_collection_for_pagination(klass, options, paginator)
    
      return paginator, collection 
    end

これをお手本に、俺paginateを書けば、何とかなりそうだ。
さっきのページにならってQuestionからcategory_idをパラメタにしてデータを引っ張ってくる例を
使うと以下のようになる。

controllerの中で。

  def paginate_questions_with_category_id(category_id, options={})
    ::ActionController::Pagination.validate_options!(:question, options, true)
    page  = params[options[:parameter]]
    count = Question.count_with_category_id(category_id)
    paginator = ::ActionController::Pagination::Paginator.new(self, count, options[:per_page], page)
    collection = Question.find_with_category_id(category_id, paginator.current.offset, options[:per_page])
    return paginator, collection 
  end

上のコードで、count_with_category_idはデータの件数を返すモデルメソッド。
find_with_category_idはファインダメソッド。どちらも中ではfind_by_sql,count_by_sqlで生SQL使ってると思ってください。
あとは、できあがった俺paginateを普通のpaginateと同じように使うだけ。
まとめると、

1. modelにoffsetとlimitを指定できるfind methodを加える。上の例でいうfind_with_category_id。第2,3引数がそれぞれoffsetとlimit。
2. modelにデータ件数を返すcountメソッドを加える。上の例でいうcount_with_category_id。
3. それらを使って俺paginateをcontrollerの中に書く。
4. 普通に俺paginateをcontrollerの中から呼ぶ。

def list
  @question_pages, @questions =
    paginate_questions_with_category_id(1, :per_page => 5)
end

5. 普通にviewでページデータを使う。

<table>
<% for question in @questions %>
  <tr><td><%=h question.message %></td></tr>
<% end %>
</table>
<%= link_to "next page", { :page => @question_pages.current.previous } if @question_pages.current.previous %>
<%= link_to "previous page", { :page => @question_pages.current.next } if @question_pages.current.next %> 

rake rails:freeze:gemsでrailsのコードを気軽に読める状態にしておくとこの手の解決法を見つけるのに便利と思った。

などと思っていたら、paginate_by_sqlなるものがあった。なるほど、その方が汎用的ですね。
まあいいか。せっかく書いたしとりあえず上げておこう。