Pagination in Rails

Pagination

Pagination is extremely important in any web application. Let’s say a client has a database with 10,000 cars for sale. You, the intrepid web developer, are hired to build a web front end to the database. You would never want to return a list of all 10,000 cars at once! When the web browser finished displaying the list (several days later), the user’s eyes would either glaze over from information overload or be left scrolling down the page for miles.

Instead you would opt for a more sensible approach and only return a subset of the list to the user—a “page”. The first page of results might have just 30 cars on it and include a link to the next page. That page would have another 30 cars listed and a link to both the next page and previous page. Or you might decide to show a list of all pages with links to allow the user to skip around. You would probably include a description on the page to communicate what part of the subset a user is viewing (e.g. “Page 1 of 20″ or “Cars 1-30 out of 10,000″). That is pagination.

Google provides a good example of pagination. Search for any word. Look at the top of the page and you’ll see the description of the page results “Results 1 - 10 of about 1,000,000 for…”. Look at the bottom of the page and you’ll see “1 2 3 4 … Next” underneath the expanding o’s in Google. This isn’t the only way to navigate paginated pages, but it illustrates the concept clearly.

Google Pagination

After the jump, I’ll demonstrate three different techniques for performing pagination on database records in Ruby on Rails.

Pagination Overview

Any paginated web page, regardless of the technology behind it, requires two sets of information—a subset of records and information about the complete set of records. The subset will be the “current page” and those records will be displayed in the browser page (usually in either a list or a grid). The information about the total set provides useful information, such as “You are viewing page 2 out of 7″ or “Displaying records 40-60 out of 100″, as well as navigation links to the other pages.

In Ruby on Rails, there are three ways to do pagination:

  • 1. Simple pagination
  • 2. Custom pagination
  • 3. Plug-in pagination

Pagination will always take place in the Controller, where you’ll both find the records in the database to display and determine the pagination information. The View will take care of displaying the results of the pagination, but the heavy lifting will already be done. For that reason, let’s hold off on looking at how to display pagination results in the View until we’ve examined the first two types of Rails pagination.

1. Simple Pagination

Simple pagination is built into Rails via the Pagination module. In simple cases, it can do the job for you. Scaffolding uses it. The paginate method performs both the find and the pagination in one step.

def list
  @car_pages, @cars = paginate(:cars,
    :conditions => 'available = 1', :order => 'price DESC', :per_page => 20)
end

Paginate takes a collection id as it’s first argument. The collection id is basically the plural, lowercase form of the Model name. In this case, it will be looking for a Model called “Car”. After the first argument, paginate takes a list of options. Most of those options are ones that can be passed to the find method (:include, :joins, :conditions, :order, :select) and that’s because paginate is going to use Model.find(:all, *args) to retrieve the records.

There is one key difference: paginate doesn’t accept :limit or :offset as options, two ways to communicate to the database which subset of records to return. That’s because paginate is going to determine those values itself using the :per_page option and the value of params[:page]. (Important Note: params[:page] is not passed as an argument to paginate but pulled from the global parameters.) It makes sense that params[:page] is the current page of records to show. :limit, or how many records to retrieve, is determined by :per_page. :offset, which dictates the starting record for retrieval is calculated as ((params[:page] - 1) * :per_page).

Paginate also does two other things besides find the records. It gets a count of all the records, similar to Model.count(*args), and it creates a Paginator object that we can use in the View. The Paginator object keeps track of all the pagination information—the total record count, the current page, how many records on this page, the total number of pages, information about the next and previous pages, etc. I’ll show you how to work with the Paginator object when we talk about the View.

Once it is done, paginate returns two items: the paginator object and a collection of the items on the current page. Because the method returns two values, it uses the double assignment syntax (x, y = paginate(z)). Don’t let that confuse you if you haven’t seen double assignment before.

# Example of double assignment syntax
def add_and_subtract( value1, value2 )
  add = value1 + value2
  subtract = value1 - value2
  return add, subtract
end

var1, var2 = add_and_subtract(10, 4)

# var1 will be 14 and var2 will be 6.

After the paginate method has made its double assignment to two instance variables, the View will be able to use them. In our example, @cars will be the collection of database records that is appropriate for this page. You can simply loop through the collection to display them, just like you would if you weren’t doing pagination. @car_pages will be an instance of the Paginator.

2. Custom pagination

If you find that the paginate method doesn’t do everything you need, you can easily build your own custom pagination. Maybe your find is complex, maybe you are using find_by_sql, or maybe you want to add more bells and whistles. Luckily creating your own isn’t much more difficult than using the paginate method.

Like the paginate method, you’ll just need to determine the total number of items in the found set, the number of items per page, and the current page. Then you will be able to find the records and create a Paginator object.

Here you’ll also want to make a choice. Will it be more efficient for you to find ALL the matching records, count them, then display just a page of the found set? Or will it be more efficient to make two calls to the database: 1) an SQL COUNT of records that would match, 2) a find of only the current records being shown on the current page. The first approach will only hit your SQL database once, but will get a lot of information during that trip. The second approach will hit your SQL database twice, but will return two smaller pieces of information on each trip. (The paginate method uses the second approach.)

The first is quicker to code but really only suitable for very small data sets. The second more robust and scalable. If I am certain that a table will never have more than 100 records in it, I use the first one. If the table could grow to eventually hold thousands of records, I use the second one.

Here’s the single-trip technique.

# Custom pagination in four easy steps, 1 SQL call
def list
    # step 1: set the variables you'll need
    page = (params[:page] ||= 1).to_i
    items_per_page = 20
    offset = (page - 1) * items_per_page

    # step 2: do your custom find without doing any kind of limits or offsets
    #  i.e. get everything on every page, don't worry about pagination yet
    @items = Item.find_with_some_custom_method()

    # step 3: create a Paginator, the second argument has to be the number of ALL items on all pages
    @item_pages = Paginator.new(self, @items.length, items_per_page, page)

    # step 4: only make a subset of @items available to the view
    @items = @items[offset..(offset + items_per_page - 1)]
end

The two-trip method is almost as easy. You’ll just need two SQL methods. In step 2 you do a count of the records that would match and then in step 4 you retrieve only the records for the current page. Keep in mind that “offset” and “items_per_page” will need to be passed to your custom find method as the :offset and :limit parameters. (Paginate does this by getting :limit from its :per_page option, and getting :offset by asking a Paginator object, like the one you create in step 3, to return paginator.current.offset. You could pass the paginator object (@item_pages) into your find method and do the same thing.)

# Custom pagination in four easy steps, 2 SQL calls (More robust!)
def list
    # step 1: set the variables you'll need
    page = (params[:page] ||= 1).to_i
    items_per_page = 20
    offset = (page - 1) * items_per_page

    # step 2: instead of performing a find, just get a count
    item_count = Item.count_with_some_custom_method()

    # step 3: create a Paginator, the second argument has to be the number of ALL items on all pages
    @item_pages = Paginator.new(self, item_count, items_per_page, page)

    # step 4: only find the requested subset of @items
    @items = Item.find_with_some_custom_method(items_per_page, offset)
end

Just like with simple pagination, both methods above result in a collection (@items) and a Paginator object (@item_pages). You can loop through the collection to display the current page of items, and you can use the Paginator object in the View to get information about the pagination.

Pagination in the View

Once your controller has assigned values to @item_pages (a Paginator instance) and @items (a collection of ActiveRecord objects), you are ready to utilize them in your view. It’s not difficult to work with the Paginator object. There is a Pagination Helper which can output all the page links for you.

<%= pagination_links(@item_pages) %>

Or you can just as easily write your own links. It is not much harder and it is more flexible. Here’s are some simple examples of how to do it for yourself.

# Previous and Next Links
# @items_pages.current is the current page being displayed by the pagination

# show a previous link if there is a previous page from the current page
<%= link_to('previous',
      :page => @item_pages.current.previous) if @item_pages.current.previous %>

# show a next link if there is a next page from the current page
<%= link_to('next',
      :page => @item_pages.current.next) if @item_pages.current.next %>

The one pitfall is to make sure that any other parameters you want to keep are also passed in the link. (You’ll notice that the scaffold’s pagination does not do this by default.)

# List of Page Numbers with Links
Pages:
<% for page in @item_pages -%>
  <%= link_to_unless((params[:page].to_i == page.number),
        page.number, :page => page) %>&nbsp;
<% end -%>
# Information about the full set and subset of records
Displaying <%= @item_pages.current_page.first_item %>
through <%= @item_pages.current_page.last_item %>
of <%= @item_pages.item_count %> items.

3. Plug-in pagination

For a few months it seemed that built-in Pagination would be revised dramatically in the upcoming Rails 2.0, but that is no longer the case. Now all pagination will be moved into a plug-in and out of the core of Rails. There, the pagination techniques above, can compete with the plug-ins below. You’ll be able to pick the one that is best for you.

There’s nothing inherently wrong with the pagination built into Rails. I think if you understand it, it will work well for you. But some developers don’t think pagination is as slick or elegant as it could be. Some of those developers have even gone one step further than custom pagination and reinvented pagination as a plug-in. Each offers different features, and each has loyal users who swear by them.

If you’ve never used plug-ins before, the Rails Wiki has a good plug-in introduction to get you started.

Conclusion

Regardless of which way you decide to go, you have a variety of pagination options available in Rails. Remember, the basic principle is always the same. You need to determine two things to paginate: the records to display on the current page and information about the rest of the data set.

My recommendation is to start with Simple Pagination. Once you understand how it works, you’ll be better prepared for further customizations. If you find it limiting or that it won’t work in a certain case, then move to Custom Pagination. Once that feels comfortable and you start looking for futher enhancements, try the plug-ins listed above. And once you outgrow those… well, then you can write your own plug-in!

12 Responses to “Pagination in Rails”

  1. Null is Love » Blog Archive » Classic Pagination R.I.P. Says:

    [...] Daigle has the news: classic pagination has been removed from Edge Rails and, as I mentioned in a previous post, classic pagination will now become a plug-in. If you are currently using Rails version 1.2.3 or [...]

  2. David Says:

    You sir, are my hero.
    Thanks for saving me a good few hours.

  3. max williams Says:

    Hi Kevin, thanks for the excellent tutorial - my list page is lovely now :)

  4. max williams Says:

    Actually Kevin i have a question - my list page is a list of articles. Given an article_id, do you know how i would get the number of the page with that article on it?

    thanks!
    max

  5. Kevin Skoglund Says:

    The only way to do that would be to scan through each of the pages looking for that article_id.

    I’m not sure what you are trying to do, but my guess is there is an easier way than determining the page with that article on it.

  6. Brian Says:

    pagination_links() can preserve additional query parameters via the :params option.

    pagination_links( @item_pages, :params => {:myfield => params[:myfield]} )

    No need to manually make your own using a for-loop. Besides you can use the pagination_links_each() method to customize the output rather than using a for-loop. If you look at the source code, you’ll see that pagination_links() wraps pagination_links_each().

  7. Jan M Says:

    I have the same problem as Max: I want to jump to the page with the last change on it after an update. Seems like a reasonable thing that lots of people want to do right?

    The only thing I can think of is reading in all article IDs and calculate the page from that and then read all the fields for just that page. Still seems wasteful for large collections. But there does not seem to be a SQL query to give you the position of a record in a sorted list so I guess that’s the only way.

  8. Jan M Says:

    Just solved my own and max’s problem, simple really:
    You can do a SELECT COUNT(*) FROM table WHERE sort_field < updated_value
    The sort_field is whatever you use in the ORDER BY clause in the paginator and updated_value is the value of that field in the recently created or updated item. Divide the result by the page size to get the page number to feed to the paginator. (Seems like a nice feature to include in a paginator library by the way…)

  9. Dinesh Says:

    Isn’t the single trip version incorrect? Seems to me it will hit the database with a full query every time a page link (or next / previous) is clicked.

    What am I not understanding about this?

  10. Kevin Skoglund Says:

    Yes, EVERY pagination technique (besides a JavaScript solution) is going to require a new query to the web server when a page link is clicked and that will result in a new SQL query.

    The “single trip version” goes to the database one time per page click and gets everything it needs to render the page contents and the pagination. It retrieves all items in the database at once.

    The “multi trip version” goes to the database two times per page click. Once to count how many items there are altogether (so it knows how many pages are possible) and a second time to get the items that show up on this page.

    But each click still results in at least one SQL call.

  11. Jonathan Says:

    Hi,
    Just to make you aware - it seems as though someone has plagiarised your content on the following website (link-removed). Great article though, thanks very much!
    jonathan

  12. Kevin Skoglund Says:

    Thanks for the heads up, Jonathan. I have written the blog’s owner. (I removed his link from your comment so I don’t compound the problem.)

Leave a Reply