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.
Blaze-Persistence supports the following pagination modes:
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:
Let’s assume a page size of 3 and a current page of 2 (1-based). Hence, the current page is:
While a user is viewing page 2, let’s assume that element
f is inserted into the list just after
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:
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:
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:
Likewise, page 2 is:
f(8) the list looks like this:
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:
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.
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
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.
firstResultis the equivalent of the offset from the previous section and
maxResultscorresponds 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
keysetPageargument is the ID set extracted from previous query results via
PagedList#getKeysetPage(). If the
firstResultargument 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.
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.