こんにちは!kossyです!
さて、今回はページネーション機能を実現するRubyのGemである「kaminari」のソースコードを読んでみたので、
ブログに残してみたいと思います。
kaminariとは
偉大なる本家リポジトリ
github.com
ページネーションを実現するGemで、Railsだけでなくsinatra等のフレームワークでも用いることができます。
後述しますが、bundle installを行うだけで、ActiveRecordを継承したクラスにkaminariのメソッドが生えるようになります。
kaminariをinstallすることで各種メソッドが定義される仕組みについて
Gemfileに以下を追加し、bundle installをすると、
gem 'kaminari' $ bundle install
以下の2つのモジュールがActiveRecordを継承したクラスにincludeされます。
Kaminari::ConfigurationMethods Kaminari::ActiveRecordModelExtension
試しにお手元のkaminariがinstallされたアプリケーションで、 rails c を実行し、
$ rails c # Userモデルが定義されていると仮定しています。 $ User.ancestors
と実行してみてください。すると、先ほど挙げた2つのモジュール名が表示されるかと思います。
bundle installするだけでこの2つのモジュールをincludeしている仕組みはどのように実現されているのでしょうか。
ソースコードを読む
まずはモジュール名からいかにもActiveRecordの拡張を行っていそうな「Kaminari::ActiveRecordModelExtension」から読んでみます。
おそらくpageメソッドを定義しているであろうこの箇所がキモですね。
included do include Kaminari::ConfigurationMethods # Fetch the values at the specified page number # Model.page(5) eval <<-RUBY, nil, __FILE__, __LINE__ + 1 def self.#{Kaminari.config.page_method_name}(num = nil) per_page = max_per_page && (default_per_page > max_per_page) ? max_per_page : default_per_page limit(per_page).offset(per_page * ((num = num.to_i - 1) < 0 ? 0 : num)).extending do include Kaminari::ActiveRecordRelationMethods include Kaminari::PageScopeMethods end end RUBY end
Kaminari.config.page_method_nameはコンソールで実行できそうなので実行してみます。
$ rails c $ Kaminari.config.page_method_name => :page
:pageが返ってきたということで、
def self.page
というメソッドがモデルに定義されることがわかりました。
定義されるタイミングについては下記ブログが詳しく解説されていました。
kaminariのactiverecord拡張部分を読む - u1f419
Kaminari::ConfigurationMethodsも確認してみます。
kaminari/configuration_methods.rb at master · kaminari/kaminari · GitHub
rails g kaminari:configとターミナルで実行することで、config/initializers/kaminari_config.rb が作成されるのですが、
そのファイルに記載された初期値かモデルで上書きした値を使うかを制御するメソッドが定義されていました。
kaminari_configの中身はこちら。
kaminari/kaminari_config.rb at master · kaminari/kaminari · GitHub
Kaminari::PageScopeMethodsをincludeすることで定義される各種メソッドの一部も見てみます。
perメソッド
# Specify the <tt>per_page</tt> value for the preceding <tt>page</tt> scope # Model.page(3).per(10) def per(num, max_per_page: nil) max_per_page ||= ((defined?(@_max_per_page) && @_max_per_page) || self.max_per_page) @_per = (num || default_per_page).to_i if (n = num.to_i) < 0 || !(/^\d/ =~ num.to_s) self elsif n.zero? limit(n) elsif max_per_page && (max_per_page < n) limit(max_per_page).offset(offset_value / limit_value * max_per_page) else limit(n).offset(offset_value / limit_value * n) end end
ActiveRecordのクエリメソッドであるlimitとoffsetを用いて、条件に応じてlimitとoffsetに渡す引数を変えることで
1ページあたりに表示する件数を制御していました。思ったよりも泥臭く愚直に実装されていますね。Gemを読み込んでいくと泥臭い処理が見られて良きです。
total_pagesメソッド
# Total number of pages def total_pages count_without_padding = total_count count_without_padding -= @_padding if defined?(@_padding) && @_padding count_without_padding = 0 if count_without_padding < 0 total_pages_count = (count_without_padding.to_f / limit_value).ceil max_pages && (max_pages < total_pages_count) ? max_pages : total_pages_count rescue FloatDomainError raise ZeroPerPageOperation, "The number of total pages was incalculable. Perhaps you called .per(0)?" end
kaminari_config.rbに定義された値を見に行きつつ、トータルのページ数を算出するメソッドでした。
まとめ
ページネーションという一般的な機能を抽象化してActiveRecordのQueryに載せるのが見事だと思いました。
勝手にpageはperなどのメソッドが生えてしまうので、それが嫌な方は自前で定義するかPagyという同じくページネーションを実現するGemを用いるのがいいかなと思います。