ページネーションを行うGem「kaminari」のソースコードを読んでみた

こんにちは!kossyです!




さて、今回はページネーション機能を実現するRubyのGemである「kaminari」のソースコードを読んでみたので、
ブログに残してみたいと思います。

github.com

環境

Ruby 2.6.6
Rails 6.0.3
MacOS catalina




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」から読んでみます。

github.com

おそらく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を用いるのがいいかなと思います。

GitHub - ddnexus/pagy: The ultimate pagination ruby gem