こんにちは!kossyです!
さて、今回はページネーション機能を実現するRubyのGemである「kaminari」のソースコードを読んでみたので、
ブログに残してみたいと思います。
github.com
kaminariをinstallすることで各種メソッドが定義される仕組みについて
Gemfileに以下を追加し、bundle installをすると、
gem 'kaminari'
$ bundle install
以下の2つのモジュールがActiveRecordを継承したクラスにincludeされます。
Kaminari::ConfigurationMethods
Kaminari::ActiveRecordModelExtension
試しにお手元のkaminariがinstallされたアプリケーションで、 rails c を実行し、
$ rails c
$ User.ancestors
と実行してみてください。すると、先ほど挙げた2つのモジュール名が表示されるかと思います。
bundle installするだけでこの2つのモジュールをincludeしている仕組みはどのように実現されているのでしょうか。
まずはモジュール名からいかにもActiveRecordの拡張を行っていそうな「Kaminari::ActiveRecordModelExtension」から読んでみます。
github.com
おそらくpageメソッドを定義しているであろうこの箇所がキモですね。
included do
include Kaminari::ConfigurationMethods
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メソッド
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メソッド
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に定義された値を見に行きつつ、トータルのページ数を算出するメソッドでした。