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 a71af7fed105d66eedb4e16d96484c71

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

diag ea01c5ac789e682474f56c5f7fc22df1

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

diag 459cd30924e9849e37ca815faefccdff

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 c86f934fd9987e9fa4e14924b5fd9d56

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 89fdd9d4519e736fa064d025402efaee

Likewise, page 2 is:

diag d6972e004ef5e2fe8263a6830649a2d6

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

diag aa2880dbba47659d9ce139e610092507

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 8d22f0079806ed2720bb6d8f593d66b2

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