Grails and the Pagination Bugaboo

Pagination in Grails is both powerful and easy... if you know what your doing.  Pagination can often be a stumbling block to new Grails converts.  In this article, I hope to shine a light on some of the darker recesses of the pagination world and provide helpful examples.  Let's get started.

Pagination Defined

Pagination is a common UI pattern used to divide large sets of data into smaller, more digestible chunks.  If you've performed a Google search then you've used pagination.  The division of those millions of search results into pages, each populated with 10, 50, or 100 results is pagination on a massive scale.

Basic Pagination

The Grails reference documentation provides a good example of how to perform pagination in the simplest of scenarios.  Click here to view it.  This covers how pagination is performed when returning a list containing all of your domain objects.  But what do you do if you only want to return a subset of these domain objects?

Advanced Pagination

Lets look at the example where I have many books by many authors and I wish to return a paginated list for only one of the authors.  Thankfully, Hibernate provides great support for this.

Example Domain Class

The domain class is the same as in the basic pagination example

class Book {
     String title
     String author
}
Example Controller

The controller is responsible for the following tasks:

  1. Specifying the author for whom books should be returned
  2. Obtaining a list of the author's books from the BookService
  3. Adding the author's total book count to params
  4. Returning the book list and params to the view
class BookController {
    def list = {
        def author = "Author Name"
        def books = bookService.getBookList(author, params)
        params.totalBooks = books.totalCount
        [bookInstanceList: books, params: params]
    }
}
Example Service

The BookService class is where the magic happens with a little help from Grails' underlying support for Hibernate's criteria query.  The service does the following:

  1. Sets the offset.  As user's click buttons provided by the paginate tag (see example below), an offset parameter is passed to the controller.  This is how we are able to determine which subset of the book list to return.
  2. Sets the maximum number of books to return.  The max value defaults to 10 if no value is provided and allows for a maximum of value of 100 (you really don't want to see more than 100 books on the screen at once, do you?).
  3. Sets the field to sort on.  In this case, we sort by the book title.
  4. Sets the sort order.  The order can be ascending or descending.  In this example, we sort by the book's title in ascending order. 
  5. Performs the criteria query, passing in the pagination specific information as well as the author for whom we'd like to retrieve books.

It's important to note that a "Book.createCriteria()" query is performed as opposed to "Book.withCriteria()".  This allows us to efficiently obtaining the total book count from the list when it is returned to the controller.

class BookService {
    def getBookList(author, params) {
        params.max = Math.min(params?.max?.toInteger() ?: 10, 100)
        params.offset = params?.offset?.toInteger() ?: 0
        params.sort = params?.sort ?: "title"
        params.order = params?.order ?: "asc"

        def books = Book.createCriteria().list(
                max: params.max,
                offset: params.offset,
                sort: params.sort, 
                order: params.order) {
            eq "author", author
        }
    }
}
Paginate Code

The paginate code is the same as in the basic example except for the inclusion of the totalBooks value. This is done because we want the totalBooks field to reflect the total book count for a particular author, not the total count of books for all authors in the database.

Summary

I hope this post helped to demystify some of the common issues surrounding pagination in Grails. Please feel free to post your comments, opinions and experiences pertaining to this subject. I'd love to hear them!

Comments:

Mr

by Kris on March 11, 2009 at 11:47 AM CDT
Hello, please add your site at www.sweebs.com where other people can find you among the best sites on the internet! Regards Kris

Mr

by rzezeski on May 27, 2009 at 3:33 PM CDT
Is this performing two queries in the background? What I think would be nice is to call Gorm w/ a dynamic query like "findByIdOrNameLike" to get the raw result set, and have it return something that I can call a method on that takes a map that tells it how to sort, order, and page the result. So something like: def rs = MyDomain.findByIdOrNameLike(23L, "%ryan%") def data = [ results: rs.paginate(params), total: rs.size ] render data as JSON I could probably write a plugin to do this, but I'm lazy :) Or, I guess if the dynamic queries had a property like the criteria query that would work too. Eg, rs.totalCount

title is confusing

by rzezeski on May 27, 2009 at 3:38 PM CDT
BTW, I was confused by the "title" form when adding my comment. I thought you meant *my* title, but on second look it seems you mean comment title? If so maybe call it something like "subject," or "heading", or be really verbose and say "comment title."

Bookservice

by martin on July 9, 2009 at 9:16 AM CDT
Nice article, but code won't work, because there is no bookService instance in the Bookcontroller

Does this work with Grails v1.0.3

by kvang on July 30, 2009 at 4:13 PM CDT
Does this work with Grails v1.0.3?

does not work

by gimi on September 4, 2009 at 7:36 AM CDT
As martin said, this does not work :( groovy.lang.MissingPropertyException: No such property: bookService for class: BookController

def bookService

by Gavingc on October 2, 2009 at 9:13 AM CDT
Yes indeed you need to create the service in grails-app/services/BookService.groovy and the add "def bookService" to the controller. Thanks for a clear and very required example Dean! Would have taken me a while to get this out of just the Grails reference.
Subject*
Name*
Comment*