Guide to Pagination with Hibernate

Learn to paginate the query results using HQL APIs and ScrollableResults interface in Hibernate. Pagination helps in cases when the number of rows in query output is very high and fetching all records will badly affect the performance of the application.

1. Using Query#setFirstResult() and Query#setMaxResults()

1.1. Setting LIMIT and OFFSET

The HQL methods Query#setMaxResults() and Query#setFirstResult() are used to limit the number of results and control pagination. It is the most common technique to implement pagination in Hibernate.

TypedQuery<EmployeeEntity> query 
	= session.createQuery("From EmployeeEntity e ORDER BY e.id", EmployeeEntity.class);

query.setFirstResult(0);
query.setMaxResults(10);

//Returns first 10 records - Page 1
List<EmployeeEntity> employeeList = query.getResultList();

Check out the SQL statement executed by Hibernate. Both methods, in fact, are used to set the LIMIT and OFFSET clauses in the SQL statements, based on the underlying database.

  • The LIMIT defines the maximum number of records returned by the query.
  • The OFFSET clause specifies how many records shall be skipped before the first record gets returned.
Hibernate: 
    select
        e1_0.ID,
        e1_0.email,
        e1_0.firstName,
        e1_0.lastName 
    from
        Employee e1_0 
    order by
        e1_0.ID offset ? rows fetch first ? rows only

Note that stored procedure queries cannot be paged with setFirstResult()/setMaxResults().

1.2. Stable Ordering

It is very important to understand that a database does not guarantee any ordering of results, by default. So to ensure the consistency of the records in the paged result sets, we must provide a ORDER BY clause to ensure a stable ordering of the records.

We can see that we have applied for order by id in our example.

TypedQuery<EmployeeEntity> query 
	= session.createQuery("From EmployeeEntity e ORDER BY e.id", EmployeeEntity.class);

1.3. Count Total Records

We can use SQL aggregate function COUNT() in the HQL query and find out the total number of records in the database.

TypedQuery countQuery = 
	session.createQuery("SELECT COUNT(e.id) from EmployeeEntity e", Long.class);

Long totalRecords = (Long) countQuery.getSingleResult();

Check out the SQL statement generated for this.

Hibernate: 
    select
        count(e1_0.ID) 
    from
        Employee e1_0

1.4. Demo

To demonstrate the pagination functionality, we have created a class to represent the paged result that can be returned to the client.

public class PaginationResult<E> {

  int currentPageNumber;
  int lastPageNumber;
  int pageSize;
  long totalRecords;
  List<E> records;

  //Setters and getters are hidden for brevity
}

Typically, in pagination, the application gets the requested page number along with the number of records. These two act as the request parameters.

We have written a function that accepts the above two arguments and returns an instance of PaginationResult that has all the information needed to build a pagination UI on the client side.

private PaginationResult<EmployeeEntity> 
	paginateUsingHql(int pageNumber, int pageSize) {

  int lastPageNumber;
  Long totalRecords;
  List<EmployeeEntity> employeeList;

  try (Session session = sessionFactory.openSession()) {
    session.getTransaction().begin();

    TypedQuery countQuery = session.createQuery("SELECT COUNT (e.id) from " +
        "EmployeeEntity e", Long.class);
    totalRecords = (Long) countQuery.getSingleResult();

    if (totalRecords % pageSize == 0) {
      lastPageNumber = (int) (totalRecords / pageSize);
    } else {
      lastPageNumber = (int) (totalRecords / pageSize) + 1;
    }

    session.getTransaction().commit();
  }


  try (Session session = sessionFactory.openSession()) {
    session.getTransaction().begin();

    TypedQuery<EmployeeEntity> query = session.createQuery("From " +
            "EmployeeEntity e ORDER BY e.id",
        EmployeeEntity.class);

    query.setFirstResult((pageNumber - 1) * pageSize);
    query.setMaxResults(pageSize);

    employeeList = query.getResultList();

    session.getTransaction().commit();
  }

  PaginationResult<EmployeeEntity> result = new PaginationResult<>();
  result.setCurrentPageNumber(pageNumber);
  result.setPageSize(pageSize);
  result.setLastPageNumber(lastPageNumber);
  result.setTotalRecords(totalRecords);
  result.setRecords(employeeList);

  return result;
}

To test if the pagination is working as expected, we have inserted total of 54 records in the database. The given code iterates 54 times and inserts an EmployeeEntity instance in each iteration.

LongStream.range(1, 55).forEach(id -> {

  EmployeeEntity employee = new EmployeeEntity();
  employee.setFirstName("FNAME_" + id);
  employee.setLastName("LNAME_" + id);
  employee.setEmail("NAME_" + id + "@email.com");

  session.persist(employee);
});

Now test the paginateUsingHql() method using JUnit Assertions.

@Test
public void testPaginationUsingHql() {

  PaginationResult<EmployeeEntity> firstPage = paginateUsingHql(1, 10);

  //@formatter:off
  Assertions.assertEquals(54, firstPage.getTotalRecords());
  Assertions.assertEquals(6, firstPage.getLastPageNumber());
  Assertions.assertEquals(10, firstPage.getRecords().size());
  Assertions.assertEquals(1, firstPage.getRecords().get(0).getEmployeeId());
  Assertions.assertEquals(10, firstPage.getRecords().get(9).getEmployeeId());

  PaginationResult<EmployeeEntity> fourthPage = paginateUsingHql(4, 10);

  //@formatter:off
  Assertions.assertEquals(54, fourthPage.getTotalRecords());
  Assertions.assertEquals(6, fourthPage.getLastPageNumber());
  Assertions.assertEquals(10, fourthPage.getRecords().size());
  Assertions.assertEquals(31, fourthPage.getRecords().get(0).getEmployeeId());
  Assertions.assertEquals(40, fourthPage.getRecords().get(9).getEmployeeId());
  //@formatter:on

  PaginationResult<EmployeeEntity> lastPage = paginateUsingHql(6, 10);

  //@formatter:off
  Assertions.assertEquals(54, lastPage.getTotalRecords());
  Assertions.assertEquals(6, lastPage.getLastPageNumber());
  Assertions.assertEquals(4, lastPage.getRecords().size());
  Assertions.assertEquals(51, lastPage.getRecords().get(0).getEmployeeId());
  Assertions.assertEquals(54, lastPage.getRecords().get(3).getEmployeeId());
  //@formatter:on
}

2. Using ScrollableResults

2.1. Scrollable result sets

The org.hibernate.Query interface offers Query#scroll() for reading query results incrementally while maintaining an open JDBC ResultSet mapped to a server-side cursor.

The scroll() method returns a ScrollableResults which wraps an underlying JDBC scrollable ResultSet.

We can use ScrollableResults#scroll() to set the starting position of the cursor and then ScrollableResults#next() to get sequentially iterate over the number of records on the page.

Query<EmployeeEntity> query 
  = session.createQuery("From EmployeeEntity e ORDER BY e.id", EmployeeEntity.class);

try (ScrollableResults resultScroll =
	query.scroll(ScrollMode.SCROLL_INSENSITIVE)) {

		do {
			EmployeeEntity employee = (EmployeeEntity) resultScroll.get();
			employeeList.add(employee);
		} while (resultScroll.next()
						&& resultScroll.getRowNumber() >= 0
						&& resultScroll.getRowNumber() <= 10);
}

It is worth noting that stable ordering of result set is still a requirement to get consistent records in each call. So it is recommended to provide the ORDER BY clause in the HQL query.

2.2. Getting Records Count

One good thing about this approach is that we do not need to execute an extra query to get the total records count. Just move the cursor to the last record in the result set, and get the current row number.

resultScroll.last();
totalRecords = resultScroll.getRowNumber() + 1;  //Cursor starts with 0

2.3. Demo

Similar to the first example, let’s build a new method that uses the ScrollableResults to paginate based on given method arguments page number and page size.

In the end, it will return an instance of PaginationResult with all the information populated into it.

private PaginationResult<EmployeeEntity> 
	paginateUsingScrollableResults(int pageNumber, int pageSize) {

  int lastPageNumber = 0;
  int totalRecords = 0;
  List<EmployeeEntity> employeeList = new ArrayList<>();


  try (Session session = sessionFactory.openSession()) {
    session.getTransaction().begin();

    Query<EmployeeEntity> query = session.createQuery("From " +
            "EmployeeEntity e ORDER BY e.id",
        EmployeeEntity.class);

    try (ScrollableResults resultScroll =
             query.scroll(ScrollMode.SCROLL_INSENSITIVE)) {
      boolean hasRecords = resultScroll.first();
      if (hasRecords) {

        int fromRecordIndex = (pageNumber - 1) * pageSize;
        int maxRecordIndex = (fromRecordIndex + pageSize) - 1;

        hasRecords = resultScroll.scroll(fromRecordIndex);

        if (hasRecords) {
          do {
            EmployeeEntity employee = (EmployeeEntity) resultScroll.get();
            employeeList.add(employee);
          } while (resultScroll.next()
              && resultScroll.getRowNumber() >= fromRecordIndex
              && resultScroll.getRowNumber() <= maxRecordIndex);
        }

        // Go to Last record.
        resultScroll.last();
        totalRecords = resultScroll.getRowNumber() + 1;
        if (totalRecords % pageSize == 0) {
          lastPageNumber = (int) (totalRecords / pageSize);
        } else {
          lastPageNumber = (int) (totalRecords / pageSize) + 1;
        }
      }
    }

    session.getTransaction().commit();
  }

  PaginationResult<EmployeeEntity> result = new PaginationResult<>();
  result.setCurrentPageNumber(pageNumber);
  result.setPageSize(pageSize);
  result.setLastPageNumber(lastPageNumber);
  result.setTotalRecords(totalRecords);
  result.setRecords(employeeList);

  return result;
}

Now we can verify the pagination functionality using similar test and assertions.

3. Conclusion

In this tutorial, we learned to implement and test the pagination functionality in hibernate using the HQL APIs. We first learned to use the query.setFirstResult() and query.setMaxResults() methods. Then we learned to use ScrollableResults.

Finally, we tested both examples using JUnit tests.

Happy Learning !!

Sourcecode on Github

Was this post helpful?

Join 7000+ Awesome Developers

Get the latest updates from industry, awesome resources, blog updates and much more.

* We do not spam !!

Leave a Comment

HowToDoInJava

A blog about Java and related technologies, the best practices, algorithms, and interview questions.