Applications that deal with retrieving and displaying data, will often run into situations where retrieving an entire dataset is just not efficient. For example, say that a database contains data for 1,000 products. On the client side, you want to display the products, but will only be displaying 25 products at a time. There will be links to next pages that will display more products. It would be inefficient to grab all 1,000 products, especially if the user never even goes past the first page. This is one of the cases where pagination of data would be appropriate.
Pagination in the context of data, is basically the process of splitting a dataset into multiple pages (or “block” of results), with a set size for each page. To get the next set of results, we would just change the “page” query parameter. Assuming you have some experience working with JPA (Java Persistence API), what that might look in code is something like
public List<Product> getProducts(int page, int size) {
return this.entityManager.createNamedQuery("Product.findAll", Product.class)
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
}
The above code is a just a method in some arbitrary service class that has access to the EntityManager
.
The call to setFirstResult
determines the first result that is returned. This allows use to paginate
by multiplying the supplied page by the size of each page. The size of the result set is determined by
the value passed to setMaxResults
. If 0 is passed as the page
, and the size
passed is 5,
then the first result would be the 0th result, and the size of the result set would be five.
If 1 was passed as the page
and 5 as the size
then the first result would be the 5th and
the result set would be the 5th through 9th.
This is pretty much how we can do pagination with vanilla JPA. This article though, will not be using vanilla JPA. My personal goto persistence framework is Spring Data JPA, which is a Spring-based abstraction layer over vanilla JPA, offering easier data management strategies. If you are unfamiliar with Spring Data, please read the previous linked documentation. You may also be interested in a previous article Why and How to Use Spring with Jersey.
Note: The source code for the examples in the article can be found in this GitHub project.
When working with Spring Data JPA, we are able to simply extend an interface
(JpaRepository
) with an interface of our own, and Spring Data will provide
implementations for some basic CRUD and paging method implementations. For example, take this interface:
public interface CustomerRepository extends JpaRepository<Customer, Long> {}
The JpaRepository
defines (or more precisely, inherits) methods such as
List<T> findAll();
T save(T t);
void delete(T t);
where T
is the entity type (in this case Customer
). These are not the only methods defined though.
There are a bunch more. And we do not need to implement these. These are all implemented by Spring Data.
Along with the “vanilla” findAll
method, there is also a “pageable” version defined:
Page<T> findAll(Pageable pageable);
This allows us to pass a Pageable
instance and returns to us a Page
. An
implementation of the Pageable
interface is the PageRequest
, which has the following
overloaded constructors:
PageRequest(int page, int size)
PageRequest(int page, int size, Sort.Direction direction, String... properties)
PageRequest(int page, int size, Sort sort)
The first constructor allows us to simply pass the page and size, just like in our vanilla JPA example.
The next two constructors allow for sorting of the pre-paged results. The sorting is based on properties
on the entity, and Sort.Direction
specifies the direction of the sort, either ascending or descending.
In our example, we will use the third constructor which allows us to pass a Sort
instance.
Let’s now implement our resource class, that makes use of the CustomerRepository
. The paging and
sorting parameters will be passed as query parameters. An example URL will be
http://localhost:8080/api/customers?page=1&size=2&sort=firstName,DESC
Here, the result will be the second page, with two results, sorted by the first name, in descending order. The following is our first implementation of the resource class:
@Path("customers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CustomerResource {
private final CustomerRepository repository;
@Autowired
public CustomerResource(CustomerRepository repository) {
this.repository = repository;
}
@GET
public Response getAllCustomers(
@QueryParam("page") @DefaultValue("0") Integer page,
@QueryParam("size") @DefaultValue("3") Integer size,
@QueryParam("sort") List<String> sort) {
List<Sort.Order> orders = new ArrayList<>();
for (String propOrder: sort) {
String[] propOrderSplit = propOrder.split(",");
String property = propOrderSplit[0]);
if (propOrderSplit.length == 1) {
orders.add(new Sort.Order(property));
} else {
Sort.Direction direction
= Sort.Direction.fromStringOrNull(propOrderSplit[1]);
orders.add(new Sort.Order(direction, property));
}
}
Pageable pageable = new PageRequest(page, size,
orders.isEmpty() ? null : new Sort(orders));
List<Customer> customers = repository.findAll(pageable).getContent();
return Response.ok(new GenericEntity<List<Customer>>(customers) {}).build();
}
}
The sort
query parameter can be used multiple times like
sort=firstName&sort=lastName,DESC
so we accept a List<String>
as the parameter type. We want to turn that List<String>
into
a List<Sort.Order>
, as that’s what the Sort
constructor takes. Remember, we are using the
third PageRequest
constructor that accepts a Sort
instance. Once we have the list, we create
the PageRequest
and pass it to the findAll
method.
The result of the findAll
call, will be a Page<Customer>
instance. Here we are just getting
the content of the page, which is the List<Customer>
. If we wanted to create some HATEOAS links,
we could use the other properties on the Page
. But we won’t be discussing HATEOAS here, so we just
return the content.
This example works fine, but we can improve it. There’s not much wrong with the code itself, but
the problem is that we would have to do this for every resource method where we wanted to add paging.
We could create a helper/utility class, but we would still have to accept the @QueryParam
s to the
resource method, then pass them to the helper. Ideally, we would rather be able to do something like
@GET
public Response getAllCustomers(@Pagination Pageable pageable) {
List<Customer> customers = repository.findAll(pageable).getContent();
return Response.ok(new GenericEntity<List<Customer>>(customers) {}).build();
}
where the Pageable
is transparently created and passed to us. In Jersey, we can do this with
a ValueFactoryProvider
, which is the SPI we need to implement to tell Jersey that we want to create
an injectable method parameter. The following is an implementation we can use for the Pageable
public class PageableValueFactoryProvider implements ValueFactoryProvider {
private final ServiceLocator locator;
@Inject
public PageableValueFactoryProvider(ServiceLocator locator) {
this.locator = locator;
}
@Override
public Factory<?> getValueFactory(Parameter parameter) {
if (parameter.getRawType() == Pageable.class
&& parameter.isAnnotationPresent(Pagination.class)) {
Factory<?> factory = new PageableValueFactory(locator);
return factory;
}
return null;
}
@Override
public PriorityType getPriority() {
return Priority.NORMAL;
}
private static class PageableValueFactory
extends AbstractContainerRequestValueFactory<Pageable> {
@QueryParam("page") @DefaultValue("0") Integer page;
@QueryParam("size") @DefaultValue("3") Integer size;
@QueryParam("sort") List<String> sort;
private final ServiceLocator locator;
private PageableValueFactory(ServiceLocator locator) {
this.locator = locator;
}
@Override
public Pageable provide() {
locator.inject(this);
List<Sort.Order> orders = new ArrayList<>();
for (String propOrder: sort) {
String[] propOrderSplit = propOrder.split(",");
String property = propOrderSplit[0];
if (propOrderSplit.length == 1) {
orders.add(new Sort.Order(property));
} else {
Sort.Direction direction
= Sort.Direction.fromStringOrNull(propOrderSplit[1]);
orders.add(new Sort.Order(direction, property));
}
}
return new PageRequest(page, size,
orders.isEmpty() ? null : new Sort(orders));
}
}
}
The main method of the ValueFactoryProvider
SPI, is the createValueFactory
method
@Override
public Factory<?> getValueFactory(Parameter parameter) {
if (parameter.getRawType() == Pageable.class
&& parameter.isAnnotationPresent(Pagination.class)) {
Factory<?> factory = new PageableValueFactory(locator);
return factory;
}
return null;
}
In order for Jersey to see if it can handle a method parameter, it will call all the
ValueFactoryProvider
s, and call their getValueFactory
methods. If the ValueFactoryProvider
can handle the parameter, it should return a Factory
, otherwise it should return null. In our
implementation we are saying that we can handle parameters of type Pageable
that are annotated
with @Pagination
(which is a custom annotation)
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pagination {
}
The PageableValueFactory
is our Factory
implementation. This will be where we create the Pageable
private static class PageableValueFactory
extends AbstractContainerRequestValueFactory<Pageable> {
@QueryParam("page") @DefaultValue("0") Integer page;
@QueryParam("size") @DefaultValue("3") Integer size;
@QueryParam("sort") List<String> sort;
private final ServiceLocator locator;
private PageableValueFactory(ServiceLocator locator) {
this.locator = locator;
}
@Override
public Pageable provide() {
locator.inject(this);
List<Sort.Order> orders = new ArrayList<>();
for (String propOrder: sort) {
String[] propOrderSplit = propOrder.split(",");
String property = propOrderSplit[0];
if (propOrderSplit.length == 1) {
orders.add(new Sort.Order(property));
} else {
Sort.Direction direction
= Sort.Direction.fromStringOrNull(propOrderSplit[1]);
orders.add(new Sort.Order(direction, property));
}
}
return new PageRequest(page, size,
orders.isEmpty() ? null : new Sort(orders));
}
}
One interesting thing about @QueryParam
, @PathParam
and other @XxxParam
, is that they can
also be injected into fields (instead of what you would normally see as method parameters). So here,
we are injecting them into this Factory
implementation. A new factory will be created for each request,
so we don’t need to worry about any concurrency issues. The only catch here, is that we need to
explicitly inject it with the ServiceLocator
. The reason is that Jersey will not inject it for us.
Now that we have our implementation, the last thing is just to register the provider with Jersey
@ApplicationPath("/api")
public class AppConfig extends ResourceConfig {
public AppConfig() {
packages("com.example");
register(new AbstractBinder() {
@Override
protected void configure() {
bind(PageableValueFactoryProvider.class)
.to(ValueFactoryProvider.class)
.in(Singleton.class);
}
});
}
}
And now we can do the following for any resource method that wants the Pageable
@GET
public Response getAllCustomers(@Pagination Pageable pageable) {
List<Customer> customers = repository.findAll(pageable).getContent();
return Response.ok(new GenericEntity<List<Customer>>(customers) {}).build();
}
Note
Earlier in this article, I mentioned something about HATEOAS and working with the Page
object.
The example above doesn’t do anything with the Page
object, and just returns the List
content.
Normally when working with pagination, you want to provide details to the client on what the next or
previous page will be, or whether there is a next, and things like that. This is where HATEOAS comes
to play. Instead of just returning the list, you may want to provide links to next and previous URLs.
For instance
http://localhost:8080/api/customers
{
"_links": {
"next": "/api/customers?page=1&size=5",
"prev": null
},
"content": [ ..first five (default size) results.. ]
}
This is just an example. HATEOAS is implemented in different ways. There is not just one correct way. I am not going to get into the details, but HATEOAS is definitely something to consider, and research to see if it’s the right fit for your application.
Support Me
If you found any information in this post useful, please show your support and like it, share it, tweet it, pin it, and/or plus one it. Much thanks!