Module jakarta.data

Interface CursoredPage<T>

Type Parameters:
T - the type of elements in this page
All Superinterfaces:
Iterable<T>, Page<T>
All Known Implementing Classes:
CursoredPageRecord

public interface CursoredPage<T> extends Page<T>

A page of data retrieved to satisfy a given page request, with a cursor for each result on the page. A repository method which returns the type CursoredPage uses cursor-based pagination to determine page boundaries.

Compared to offset-based pagination, cursor-based pagination reduces the possibility of missed or duplicate results when records are inserted, deleted, or updated in the database between page requests. Cursor-based pagination is possible when a query result set has a well-defined total order, that is, when the results are sorted by a list of entity fields which forms a unique key on the result set. This list of entity fields must be the entity fields of the combined sort criteria of the repository method, in the same order of precedence. This could just be the identifier field of the entity, or it might be some other combination of fields which uniquely identifies each query result.

When cursor-based pagination is used, a next page request is made relative to the last entity of the current page and a previous page request is made relative to the first entity of the current page. Alternatively, a page request might be made relative to an arbitrary starting point in the result set, that is, with an arbitrary value of the key.

The key for a given element of the result set is represented by an instance of Cursor.

To use cursor-based pagination, declare a repository method with return type CursoredPage and with a special parameter (after the normal query parameters) of type PageRequest, for example:

 @OrderBy("lastName")
 @OrderBy("firstName")
 @OrderBy("id")
 CursoredPage<Employee> findByHoursWorkedGreaterThan(int hours, PageRequest pageRequest);
 

In initial page may be requested using an offset-based page request:

 page = employees.findByHoursWorkedGreaterThan(1500, PageRequest.ofSize(50));
 

The next page may be requested relative to the end of the current page, as follows:

 page = employees.findByHoursWorkedGreaterThan(1500, page.nextPageRequest());
 

Here, the instance of PageRequest returned by nextPageRequest() is based on a key value encapsulated in an instance of Cursor and identifying the last result on the current page.

A PageRequest based on an explicit cursor may be constructed by calling PageRequest.afterCursor(PageRequest.Cursor). The key component values of the cursor supplied to this method must match the list of sorting criteria specified by OrderBy annotations or OrderBy name pattern of the repository method and the Sort and Order parameters of the repository method. For example:

 Employee emp = ...
 PageRequest pageRequest =
         PageRequest.ofPage(5)
                    .size(50)
                    .afterCursor(Cursor.forKey(emp.lastName, emp.firstName, emp.id));
 page = employees.findByHoursWorkedGreaterThan(1500, pageRequest);
 

By making the query for the next page relative to observed values, instead of to a numerical position, cursor-based pagination is less vulnerable to changes made to data in between page requests. Adding or removing entities is possible without causing unexpected missed or duplicate results. Cursor-based pagination does not prevent misses and duplicates if the entity properties which are the sort criteria for existing entities are modified or if an entity is re-added with different sort criteria after having previously been removed.

Cursor-based Pagination with @Query

Cursor-based pagination involves generating and appending additional restrictions involving the key fields to the WHERE clause of the query. For this to be possible, a user-provided JDQL or JPQL query must end with a WHERE clause to which additional conditions may be appended.

Sorting criteria must be specified independently of the user-provided query, either via the OrderBy annotation or, or by passing Sort or Order. For example:

 @Query("WHERE ordersPlaced >= ?1 OR totalSpent >= ?2")
 @OrderBy("zipcode")
 @OrderBy("birthYear")
 @OrderBy("id")
 CursoredPage<Customer> getTopBuyers(int minOrders, float minSpent,
                                     PageRequest pageRequest);
 

Only queries which return entities may be used with cursor-based pagination because cursors are created from the entity attribute values that form the unique key.

Page Numbers and Totals

Page numbers, total numbers of elements across all pages, and total count of pages are not accurate when cursor-based pagination is used and should not be relied upon.

Database Support for Cursor-based Pagination

A repository method with return type CursoredPage must raise UnsupportedOperationException if the database itself is not capable of cursor-based pagination.

  • Method Details

    • cursor

      PageRequest.Cursor cursor(int index)
      Returns a Cursor for key values at the specified position.
      Parameters:
      index - position (0 is first) of a result on the page.
      Returns:
      cursor for key values at the specified position.
    • hasPrevious

      boolean hasPrevious()
      Returns true when it is possible to navigate to a previous page of results or if it is necessary to request a previous page in order to determine whether there are more previous results.
      Specified by:
      hasPrevious in interface Page<T>
      Returns:
      false if the current page is empty or if it is known that there is not a previous page.
    • nextPageRequest

      PageRequest nextPageRequest()

      Creates a request for the next page in a forward direction from the current page. This method computes a cursor from the last entity of the current page and includes the cursor in the pagination information so that it can be used to obtain the next page in a forward direction according to the sort criteria and relative to that entity.

      Specified by:
      nextPageRequest in interface Page<T>
      Returns:
      pagination information for requesting the next page.
      Throws:
      NoSuchElementException - if the current page is empty or if it is known that there is no next page. To avoid this exception, check for a true result of Page.hasNext() before invoking this method.
    • previousPageRequest

      PageRequest previousPageRequest()

      Creates a request for the previous page in a reverse direction from the current page. This method computes a cursor from the first entity of the current page and includes the cursor in the pagination information so that it can be used to obtain the previous page in a reverse direction to the sort criteria and relative to that entity. Within a single page, results are not reversed and remain ordered according to the sort criteria.

      Page numbers are not accurate and should not be relied upon when using cursor-based pagination. Jakarta Data providers should aim to at least avoid returning negative or 0 as page numbers when traversing pages in the previous page direction (this might otherwise occur when matching entities are added prior to the first page and the previous page is requested) by assigning a page number of 1 to such pages. This means that there can be multiple consecutive pages numbered 1 and that navigating to the previous page and then forward again cannot be relied upon to return a page number that is equal to the current page number.

      Specified by:
      previousPageRequest in interface Page<T>
      Returns:
      pagination information for requesting the previous page.
      Throws:
      NoSuchElementException - if the current page is empty or if it is known that there is no previous page. To avoid this exception, check for a true result of hasPrevious() before invoking this method.