Spring Web MVC and the benefits of pagination

Lately I’ve been working on a project that keeps my web bookmarks sync’d across multiple computers and different web browsers. The project is implemented in Java using JBoss 4.2.2, MySQL 5.0 and Spring 2.0. It’s not a very big project right now, only about 40 class files and some small number of configuration files, but the amount of data the application deals with can get fairly large.

Since this app isn’t the only way to manage ones bookmarks that everyone has used since the dawn of the web, a feature to import bookmarks from browsers was very high on the to-do list. So across the set of different browsers I’ve used on the different computers, and the backed up bookmarks that survived system formats I have around 700 bookmarks in total. That probably isn’t a whole lot, but that many spread across thousands of users can add up quickly.

I used jhat to find out that each bookmark object uses 1.5 kilobytes of memory. It doesn’t take much to see that a LOT of bookmarks will fit into 8 gigs of memory on a medium sized server. It would take hundreds of thousands of users to generate enough bookmark data to fill that memory. At this point it’s clear the application server box can handle the data load (that isn’t to say that it can handle the network load, however).

I spent some time coming up with a nice data model and ORM design so accessing the data once it’s in memory is lightning fast, maybe even faster. But what about when it’s on a hard disk, deep in the database? Dragging my 700 bookmarks out of the database when I only want to view the 25 most frequently visited is a bit overkill, especially when you consider dragging everyone else’s bookmarks out at the same time. Even though this is still a small-time application I decided to be nice to the database anyway and implement some kind of paging solution. Spring Web MVC doesn’t offer much that I’m aware of in regard to pagination help so it looks like I’m on my own.

Pagination is something that must take place in the model with the help of the view. Right away we’ve got two things to deal with, though it’s best to deal with them in isolation. The first thing I did was get the model right. I’m using Hibernate for ORM and the Criteria API makes it easy to set offsets and limits. That is, I can ask for the 50 next items (limit) starting at row 20 (offset).

The Criteria API is great at getting your data into a workable format without HQL or SQL (not that either of them is a bad thing. I enjoy working with them). Let’s take a look at the method that gets some new bookmarks.


public List<Bookmark> getNewestBookmarks(int offset, int numBookmarks) {
DetachedCriteria c = DetachedCriteria.forClass(Bookmark.class);
c.addOrder(Order.desc(“createdDate”));
return getHibernateTemplate().findByCriteria(c, offset, numBookmarks);
}

So this method gets the model for us, but where do the offset and limit come from? From the controller, of course. This method is called from the controller like so:


public ModelAndView newest(HttpServletRequest request, HttpServletResponse response) {
int numberOfBookmarks = requestUtils.getSafeValueFromRequest(request, “numberOfBookmarks”, 250, defaultNumberOfNewest);
int offset = requestUtils.getSafeValueFromRequest(request, “offset”, 300, 0);

List bookmarks = bookmarkService.getNewestBookmarks(offset, numberOfBookmarks);

Map model = requestUtils.generateBookmarkModel(offset, numberOfBookmarks, 300, bookmarks);
model.put(“bookmarkListName”, “Newest Bookmarks”);
return new ModelAndView(“index”, model);
}

After sanity checking the offset and limit received from the request they are passed on to the getNewestBookmarks method to create and execute a somewhat unsightly query. At this point, rather than gathering up all 700 of my bookmarks only 25 or so are returned. What if I one day want to view the top 50 most visited bookmarks though? Or what if I want to scroll through them one page at a time? Passing in 0, 25 for the offset and limit of every method call won’t cut it. The view needs to tell us how many bookmarks to retrieve and where to start the search.

Let’s say I’m viewing the top 25 bookmarks in my collection and I want to view 26-50. Somehow the view has to tell the controller to tell the model about the desired offset and limit. This can be done by passing them in as request parameters. In the code sample above, the HttpServletRequest object passed in to the controller contains values for offset and limit (numberOfBookmark) variables. The JSP code below shows how to pass these parameters when a user clicks a link. (Actually I’m having a hard time getting the JSP to show up right when the code is rendered as HTML. Just click the link and view the entire file instead.)

JSP Pagination Operations

This page uses the JSTL core tag library to build the links that display the “previous/next/more/less” options. Right after the body tag there is code to create four URLs, one for each action. Each one starts off with a name and a value. c:url var=”viewMoreUrl” value=”" creates a page-scoped variable called viewMoreUrl. This variable will later be used to display the link that allows me to display five more bookmarks when I view the list. The value is left blank which tells the tag to use the current base URL in creating the new one. If I’m on a page that is located at /bookmarks/pages/viewUserBookmarks.html the new URL will start off as /bookmarks/pages/viewUserBookmarks.html.

Adding parameters to a URL is easy. Using the c:param tag we get c:param name=”numberOfBookmarks” value=”${numberOfBookmarks + 5}”. This tag appends a variable called numberOfBookmarks to the base URL described in the previous paragraph. The value is a JSP expression that uses the numberOfBookmarks value passed in the model and increases it by five to pass back in the request. Because we want the link only to display five more bookmarks and not change the offset that value will remain the value passed out in the model. That gives us a URL that looks like /bookmarks/pages/viewUserBookmarks.html?numberOfBookmarks=30&offset=0.

When this URL is accessed the request values for offset and numberOfBookmarks are exposed in the HttpServletRequest object for use by the controller. When the getNewestBookmarks method is called the offset and limit are 0 and 30 instead of 0 and 25.

Obtaining a new offset value is done like getting a new limit value. We start off with the same base URL and leave the numberOfBookmarks value unchanged. We need to change the offset value though. c:param name=”offset” value=”${offset + numberOfBookmarks}” creates the offset value of the URL. If we are viewing the newest bookmarks (offset 0) and we are viewing 30 bookmarks when we click the link we should see bookmarks 31-60.

Here is an interesting thing to consider: what if a user goes to the second page using the default values. The user will be viewing bookmarks 26-50. The user then clicks the “view more” link three times and is now viewing 40 bookmarks. The “previous” will now request that the offset be -15. Unfortunately we can’t have negative bookmarks. In this case the controller has some sanity checking built in to it. The input parameter values must always be validated before they are used. The controller uses a utility method to get a safe value for the offset and limit and ensures that neither can be a negative value. This safe value is then passed back to the view once the model has been built. Even though the offset in the request says -15 the controller will only go as low as 0. More interesting work takes place in the generateBookmarksModel method, which I will talk about more next week.

What did I learn from making this effort? Well, giving 8GB of RAM to an application server is a little overkill for a domain model like this. The pagination that I came up with is still rudimentary, but a little more work could add more features, namely displaying page number links and the ability to change the number of bookmarks added at a time.

What is gained from this effort? Being nice to the database opens up a few doors. It dramatically improves scalability because it allows these requests to be stateless. If an app server queried the database for all of my bookmarks then my session would have to be directed to that app server every time I made a request in order to not incur the cost of hitting the database again by another app server. If round-robin load balancing is used the Hibernate session on each app server would contain a copy of my data and that’s a waste of resources, most notably it’s a hit to the database that didn’t have to happen.

Reducing the load on the database by getting only what we need and stateless sessions gives me the option of scaling the database with less difficulty. A mostly-read application can get by with fewer hits to the write master and do a majority of their work from read-only slaves. Spreading complex queries over multiple read slaves is much preferable to every app server hitting one database for every query. By having stateless requests I can have more app servers (using less memory) hitting more database servers without maxing out the CPU utilization due to the cost of complex queries.

What should I do with the unused memory in the app server boxes? Rather than give it to one app server instance for use as a cache it should be given to the pool of application servers for use as a cache. One way to do that is with memcached. I’ll talk about that more in the future too.

As always please leave comments. I enjoy getting feedback on what I’ve written.

1 comment so far ↓

#1 toolman on 04.02.08 at 5:28 pm

The example JSP showing the pagination calculation is sweet. Nice writeup just what I need.

All you need is more white space in your writing :)

Leave a Comment