Railsにおいて,kaminari
やwill_paginate
のようなページネーションライブラリは非常に強力だ.
というか,あまりページネーションというものを意識せずとも使えてしまうので,便利だ.
ページネーション時のカウントクエリが重い
便利ではあるのだが,すべての場合において無敵なわけではない.
複雑なJOIN
を繰り返すことによりインデックスが効かなくなった状態で,レコード数が大量にあるような場合.そんなときに,これらのページネーションを使っていると,重くなる場合がある.
ページネーションライブラリは,ページの下部にページネーションのビューを表示している. そして,ここにはラストページの番号が書かれている.
この数字は,ページネーションライブラリ内部でカウントクエリを発行して,条件に合致するレコード数をカウントしている.
複雑で重いクエリの場合,このカウントクエリが非常に重くなってしまう場合がある.
大量のレコードがある場合のページネーション
gemにした
このような場合のページネーションを上手いこと解決してくれるgemを作った.
方針
そもそもそのページ数,正確である必要は?
カウントクエリが重くなるような状態では,たいていの場合はレコード数が多い.例えば4000ページくらいあったとしよう.果たして1ページ目を訪れる人のどのくらいが,最終ページにいくだろうか? そして最終ページにいくとき,そのページ数が正確である必要性はあるのだろうか?
これは俺の個人的意見なのだが,ページ数もレコード件数も,そこまで正確である必要はないと思っている.だいたい有効桁数3桁くらいまで出ていれば,その先はなんであろうと変わらない. そもそも天下のgoogleだって,検索件数を正確には出していないじゃないですか.
どう実現するか
- 本当に一番最初に1ページ目を表示したときに,ページ数をCOUNT クエリ発行してカウントする
- その値を(クエリをKeyとして)Redisに保存しておく
- ラストページの値は,最初にカウントした値を元に,多めに見積もって適当にceil して出しておく
- アクセスされたとき,もしレコード件数が0件だったら,もう一度COUNT クエリを発行して正確なラストページの値を算出し,ラストのレコードを表示する
- そのとき,Redisに保存されている値と違う値になっていたら,Redisの値を更新する
使い方
READMEに書いてあることとほとんど同じですが.
Redisの設定
config/initalizers/redis.rb
みたいなファイルを作ってください.
GuessPaging::RedisClient.setup do |config| config.redis_host = '127.0.0.1' config.redis_port = 6379 end
Controllers
class RecordsController < ApplicationController def search @guess = GuessPaging::Paginate.new( query: Record.where(category_id: params[:category_id].to_i), per_page: 10, essential: 3) @guess.guess( page_params: params[:page] ) end end
per_page
はわかると思います.ちなみにデフォルトで10が設定されているので,省略可能なオプションです.
essential
は,ページ数を適当に丸めて表示するときの有効桁数です.この例で言うと,ラストが4561ページだったら,4570と表示されることになります.この値も,デフォルト値として3が設定されているので,省略可能なオプションになります.
Views
<% @guess.records.each do |r| %> <%= r.hogehoge %> <% end %> <%= paging(@guess) %>
元のActiveRecord
オブジェクトはrecords
というインスタンス変数に格納されています.ここから取り出せば,今までと同じように使えます.
Assets
app/assets/stylesheets/application.css
に以下の行を追加します.
//= require 'guess_paging'
これでこんな感じのビューデザインになります.
Helpers
ページネーションのビューを表示する
<%= paging(@guess) %>
現在のページ番号
@guess.current_page
ラストページの番号(概数)
@guess.max_page
レコード件数(概算)
@guess.count
速くなるよ
あまり複雑なDBを即席で作れなかったので,適当に10万件くらいの軽いレコードなので,そこまで大きな差は出ていませんが.
kaminari
guess_paging
できていないこと
localeによるビューの変更
kaminari
のようにlocaleの設定でページネーションビューの表示文字を変更することについては,対応していません.デザイン変更 これは使う側で適当なcssを作ってもらえばもちろん上書きできます.デフォルトだと,
//= require 'guess_paging'
したものがそのまま適応されます.
思うところ
今回のgemはActiveRecord
の拡張として作ってはいません.どうもページネーションの機能がActiveRecord
に拡張されて入るというのは,違う気がしていてい,ページネーションはページネーションのオブジェクトを生成するように作りました.
ただ,これにもちょっとだけ不便なところがあって,ActiveRecord
の拡張として作ると,呼び出された時に初めてDBアクセスしてARのオブジェクトが生成されます.
なので,コントローラでクエリを書いておくと,ビューで呼び出すときに初めてDBアクセスされますよね.そのため,kaminari
のようなページネーションでは,ページネーション用のカウントクエリはビューで呼び出した際に初めてクエリが呼ばれます.
それに対して,guess_paging
は,ARで書いたクエリを一度GuessPaging
に渡して,そこでオブジェクト化しているので,guess
メソッドを読んだ時点でARのオブジェクト生成が走り,必要なクエリが呼ばれます.
なるほど,ARの拡張として作るというのにはそういう意味があるんですね.
もうひとつ考えたこととしては,おそらくこのgemってレコード件数が少ないページにはあんまり使い勝手が良くないんですよね. だって,勝手にページ数保持するじゃないですか,正確じゃないでしょ. これを10ページとかでやられたら,むしろ邪魔なわけですよ.
そうなると kaminari
や will_paginate
と同居するってことは十分に考えられて,その際にARの拡張にしているとメソッド名被りそうだな……と.
であるならオブジェクトからして別の方が取り回しが楽な予感がしました.