Pagination powered by Blaze-Persistence

Introduction to pagination modes and comparison of benefits in Blaze-Persistence

By Moritz Becker on 24 October 2016

Blaze-Persistence comes with great support for pagination. In this blog post I describe the different pagination modes that are supported and how to implement them with Blaze-Persistence.

Pagination Types

Blaze-Persistence supports the following pagination modes:

  • Offset pagination

  • Keyset pagination

Offset pagination

Imagine your paginated data as an array of elements with a beginning and an end. For example, this array might be the content of a table in your database that you want to display pagewise. Offset pagination works by specifying an offset from the beginning of the array indicating the start of the current page. This can be problematic when elements are inserted into your array while users are paging through it.

For example, consider the following array:

diag 051aac885c8b828de5c7543e4e8a85dd

Let’s assume a page size of 3 and a current page of 2 (1-based). Hence, the current page is:

diag 53570a0990de4826ff0a719d8e57a558

While a user is viewing page 2, let’s assume that element f is inserted into the list just after o:

diag d0fa81a1f7ca0f6d04f4b0704e8ee675

The user then loads the next page. Internally, this can be accomplished by accessing the data array with an offset of cur_offset + pagesize = 6. The resulting page is:

diag 67d064c21d44f650732dd851d755e079

So the problem with this type of pagination is that existing elements are shifted between pages as new elements are inserted. From a user’s perspective, this looks like there are two instances of a in the data.

To fix this situation, we would somehow need to observe the insertion of new elements. We would then need to check if the position of the new element is less than cur_offset + pagesize:

  • less: increment cur_offset

  • greater or equal: no action required

Depending on your data store, such an observation of new elements can be tricky to realize and unreliable. For a seemingly trivial use case such as data pagination, it certainly would be overkill.

Another disadvantage inherent to offset pagination comes from the performance perspective. Since we display a single page based on an offset from the beginning of the list, we need to know all the data that precedes the desired page. So when fetching from the database, the database must internally calculate the complete result set up to and including the desired page to be able to subsequently apply the offset value and page size in a meaningful way.

Keyset pagination to the rescue

The issues faced with offset pagination can be prevented when pagination is performed relative to the element IDs in the last page.

Let’s adapt our previous example by assigning unique IDs to our list elements:

diag 7264e3ba2462641c805e91124d0cbeb3

Likewise, page 2 is:

diag eabd8981137ed64ef5f3a412f7b1a1c4

After inserting f(8) the list looks like this:

diag 59c6e09caf07862e444cd26b18f1ca75

While residing on page 2 the next page is now determined based on the last ID in page 2 and on the page size. In SQL terminology we just apply a constraint where id > last_id limit page_size. Based on this logic, page 3 is now:

diag 469e855ed34eb5b2ff018856790a3124

It is also important to notice that we did not require knowledge of the complete list of elements preceding page 3 in order to build the page. Shortly, both effectiveness and efficiency of pagination are improved when using keyset pagination.

You might also want to check out this great blog post about keyset pagination - be aware that it is in German.

Code examples

Now that you know the ideas behind the different pagination modes, I will show you how to do pagination using Blaze-Persistence. There are 2 APIs provided by Blaze-Persistence depending on whether you use entity views or not.

Generally, with both APIs you end up with a PaginatedCriteriaBuilder that returns a PagedList when calling getResultList(). PagedList#getKeysetPage() provides access to the ID set that can be used for keyset pagination in subsequent queries.

Pagination without entity views

The default CriteriaBuilder provides the page() method for pagination in three flavours:

page(int firstResult, int maxResults)

This method performs simple offset pagination. firstResult is the equivalent of the offset from the previous section and maxResults corresponds to the page size.

page(Object entityId, int maxResults)

Use this method to navigate to the page that contains a specific entity with ID entityId. If an entity with the given ID exists, the returned page starts with this entity.

page(KeysetPage keysetPage, int firstResult, int maxResults)

This is the entrypoint to keyset pagination. The keysetPage argument is the ID set extracted from previous query results via PagedList#getKeysetPage(). If the firstResult argument is chosen such that keyset pagination is not possible, a transparent fallback to offset pagination is performed.

// initial query using offset pagination
PagedList<Cat> page3 = cbf.create(em, Cat.class)
    .orderByAsc("id") // unique ordering is required for pagination
    .page(6, 3)
    .withKeysetExtraction(true)
    .getResultList();

// keyset pagination for querying the next page
PagedList<Cat> page4 = cbf.create(em, Cat.class)
    .orderByAsc("id")
    .page(page3.getKeysetPage(), 9, 3)
    .withKeysetExtraction(true)
    .getResultList();

// keyset pagination for querying the previous page
PagedList<Cat> page2 = cbf.create(em, Cat.class)
    .orderByAsc("id")
    .page(page3.getKeysetPage(), 3, 3)
    .withKeysetExtraction(true)
    .getResultList();
Pagination with entity views

Blaze-Persistence allows pagination in combination with entity views via the EntityViewSetting#create methods. Since the API is analogous to the one from CriteriaBuilder#page(), I will skip the API description and jump straight to the code example instead.

CriteriaBuilder<Cat> baseQueryBuilder = cbf.create(em, Cat.class)
    .orderByAsc("id");

// initial query using offset pagination
EntityViewSetting<CatView, PaginatedCriteriaBuilder<CatView> setting =
    EntityViewSetting.create(CatView.class, 6, 3);
PagedList<CatView> list1 = evm.applySetting(setting, baseQueryBuilder)
    .withKeysetExtraction(true)
    .getResultList();

// keyset pagination for querying the next page
EntityViewSetting<CatView, PaginatedCriteriaBuilder<CatView> setting =
    EntityViewSetting.create(CatView.class, 9, 3);
PagedList<CatView> list2 = evm.applySetting(setting, baseQueryBuilder)
    .withKeysetExtraction(true)
    .getResultList();

// keyset pagination for querying the previous page
EntityViewSetting<CatView, PaginatedCriteriaBuilder<CatView> setting =
    EntityViewSetting.create(CatView.class, 3, 3);
PagedList<CatView> list3 = evm.applySetting(setting, baseQueryBuilder)
    .withKeysetExtraction(true)
    .getResultList();

For more examples, check out the showcases on GitHub.

Conclusion

This blog post explained the 2 pagination modes supported by Blaze-Persistence and provided concrete usage examples. Pagination is an issue in almost any application and I hope this article could provide insights for you on how to do it right. If you have questions, contact us on Slack or leave a comment.

blaze-persistence pagination keyset offset entity-view
comments powered by Disqus