/ Tags: RAILS-CODE / Categories: SOLUTIONS

Paginate Activerecord Results Without A Gem Using Limit And Offset

Kaminari and Pagy are excellent, but a JSON API or an internal admin screen often needs nothing more than a page number and a page size. ActiveRecord gives you that in two methods.

Description

limit sets the page size and offset skips the preceding pages. The arithmetic is offset = (page - 1) * per_page. Always clamp the inputs. An unbounded per_page from a query string lets anyone request a million rows, and a negative page produces a negative offset and a SQL error. Always add a deterministic order. Without one, PostgreSQL makes no ordering guarantee between queries, so rows can appear on two pages or none. Note that OFFSET gets slower as the offset grows, because the database still scans the skipped rows. Past a few thousand pages, switch to keyset pagination on an indexed column.

Sample input:

  Order.page_of(page: 3, per_page: 25)


Sample Output:

  SELECT orders.* FROM orders ORDER BY created_at DESC, id DESC LIMIT 25 OFFSET 50

Answer

  class ApplicationRecord < ActiveRecord::Base
    self.abstract_class = true

    MAX_PER_PAGE = 100

    scope :page_of, ->(page: 1, per_page: 25) {
      page     = [page.to_i, 1].max
      per_page = per_page.to_i.clamp(1, MAX_PER_PAGE)

      limit(per_page).offset((page - 1) * per_page)
    }
  end

  # Usage — note the explicit, deterministic order
  orders = Order.order(created_at: :desc, id: :desc)
                .page_of(page: params[:page], per_page: params[:per_page])

  # Pagination metadata for the response
  total = Order.count
  render json: {
    data: orders.as_json,
    meta: {
      page: page,
      per_page: per_page,
      total: total,
      total_pages: (total / per_page.to_f).ceil
    }
  }

  # Keyset pagination — constant time regardless of depth
  scope :after_cursor, ->(created_at, id) {
    where("(created_at, id) < (?, ?)", created_at, id)
      .order(created_at: :desc, id: :desc)
  }

Learn More

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More