如无意外,kaminari就是monkey patch了actionview和activerecord

require 'kaminari/core'
require 'kaminari/actionview'
require 'kaminari/activerecord'


对activerecord的patch基本上就是include Kaminari::ActiveRecordModelExtension

# frozen_string_literal: true
require 'kaminari/activerecord/active_record_relation_methods'

module Kaminari
  module ActiveRecordModelExtension
    extend ActiveSupport::Concern

    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
  end
end


由此获得page方法,它所做的就是帮你做limit和offset,并extend一些方法



extending方法来源于ActiveRecord::QueryMethods,它会mixin到Relation中

它用途就是动态生生module然后extend当前Relation

def extending(*modules, &block)
  if modules.any? || block
    spawn.extending!(*modules, &block)
  else
    self
  end
end

def extending!(*modules, &block) # :nodoc:
  modules << Module.new(&block) if block
  modules.flatten!

  self.extending_values += modules
  extend(*extending_values) if extending_values.any?

  self
end


extending与inlcudes、joins之类同属于MULTI_VALUE_METHODS(暂不明为何这样归类)

module ActiveRecord
  class Relation
    MULTI_VALUE_METHODS  = [:includes, :eager_load, :preload, :select, :group,
                            :order, :joins, :left_joins, :left_outer_joins, :references,
                            :extending, :unscope]


extending_values定义如下

module ActiveRecord
  module QueryMethods

    # ...

    FROZEN_EMPTY_ARRAY = [].freeze
    Relation::MULTI_VALUE_METHODS.each do |name|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}_values
          @values[:#{name}] || FROZEN_EMPTY_ARRAY
        end

        def #{name}_values=(values)
          assert_mutability!
          @values[:#{name}] = values
        end
      CODE
    end


view中paginate的大概执行过程如下


它使用total_count来计算页码

module Kaminari
  module Helpers
    module HelperMethods

      def paginate(scope, paginator_class: Kaminari::Helpers::Paginator, template: nil, **options)
        options[:total_pages] ||= scope.total_pages
        options.reverse_merge! current_page: scope.current_page, per_page: scope.limit_value, remote: false

        paginator = paginator_class.new (template || self), options
        paginator.to_s
      end


total_count就是排除掉limit和offset后的Relation的count(如果已loaded且发现不足一页,则直接返回条数)

def total_count(column_name = :all, _options = nil) #:nodoc:
  return @total_count if defined?(@total_count) && @total_count

  # There are some cases that total count can be deduced from loaded records
  if loaded?
    # Total count has to be 0 if loaded records are 0
    return @total_count = 0 if (current_page == 1) && @records.empty?
    # Total count is calculable at the last page
    per_page = (defined?(@_per) && @_per) || default_per_page
    return @total_count = (current_page - 1) * per_page + @records.length if @records.any? && (@records.length < per_page)
  end

  # #count overrides the #select which could include generated columns referenced in #order, so skip #order here, where it's irrelevant to the result anyway
  c = except(:offset, :limit, :order)
  # Remove includes only if they are irrelevant
  c = c.except(:includes) unless references_eager_loaded_tables?
  # .group returns an OrderedHash that responds to #count
  c = c.count(column_name)
  @total_count = if c.is_a?(Hash) || c.is_a?(ActiveSupport::OrderedHash)
    c.count
  else
    c.respond_to?(:count) ? c.count(column_name) : c
  end
end


controller部分的完整trace如下

(view的跑太久了未跑完)

kaminari_controller.html