Providing pagination for displaying large set of data will improve user experience and reduce the response time. Spring Boot comes with out-of-the-box solution for handling pagination, so let’s check how to implement it in an example application.
In our example API we want to fetch time travellers on the "/api/timetravellers"
endpoint, but because the list can be elongated, we need to paginate the results.
We are working with:
TimeTraveller
entity with id
and name
properties,TimeTravellerRepository
,TimeTravellerService
for fetching data,TimeTravellerController
for providing the endpoint.The following fragment of the pom.xml
file shows dependencies used in the project:
// pom.xml … <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> …
To allow paginating requested data we have to make sure that TimeTravellerRepository
extends the PagingAndSortingRepository interface:
//TimeTravellerRepository.java import org.springframework.data.repository.PagingAndSortingRepository; public interface TimeTravellerRepository extends PagingAndSortingRepository<TimeTraveller, Long> { }
Now the TimeTravellerService
method can accept Pageable
instance as an argument and simply pass it when calling findAll()
method on the repository:
//TimeTravellerService.java import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; … @Service public class TimeTravellerService { private TimeTravellerRepository travellerRepository; public TimeTravellerService(TimeTravellerRepository travellerRepository) { this.travellerRepository = travellerRepository; } … public Page<TimeTraveller> getTimeTravellers(Pageable pageable) { return travellerRepository.findAll(pageable); } …
Check out the simple example test for this functionality in TimeTravellerServiceTest
:
// TimeTravellerServiceTest.java … import in.keepgrowing.eternityproject.TimeTraveller; import in.keepgrowing.eternityproject.TimeTravellerRepository; import in.keepgrowing.eternityproject.TimeTravellerService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.*; @RunWith(MockitoJUnitRunner.class) public class TimeTravellerServiceTest { @Mock private TimeTravellerRepository travellerRepository; private TimeTravellerService travellerService; @Before public void setUp() { travellerService = new TimeTravellerService(travellerRepository); } … @Test public void getsPagedTimeTravellers() { int pageNumber = 0; int pageSize = 1; Pageable pageable = PageRequest.of(pageNumber, pageSize); TimeTraveller traveller = new TimeTraveller("James Cole"); Page<TimeTraveller> travellerPage = new PageImpl<>(Collections.singletonList(traveller)); when(timeTravellerRepository.findAll(pageable)).thenReturn(travellerPage); Page<TimeTraveller> travellers = timeTravellerRepository.findAll(pageable); assertEquals(travellers.getNumberOfElements(), 1); }
We are going to handle the following request:
http://localhost:8080/api/timetravellers?page=0&size=5&sort=id,asc
The controller endpoint looks straightforward:
//TimeTravellerController.java … import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @RestController @RequestMapping("api/timetravellers") public class TimeTravellerController { … @GetMapping public Page<TimeTraveller> getTimeTravellers(Pageable pageable) { return timeTravellerService.getTimeTravellers(pageable); } …
And below you can find the complementary test for the controller endpoint:
//TimeTravellerControllerTest … import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @WebMvcTest(TimeTravellerController.class) @EnableSpringDataWebSupport public class TimeTravellerControllerTest { private final String apiPath = "/api/timetravellers"; @MockBean private TimeTravellerService travellerService; @Autowired private MockMvc mvc; private JacksonTester<TimeTraveller> travellerJacksonTester; @Before public void setUp() { JacksonTester.initFields(this, new ObjectMapper()); } @Test public void getsTimeTravellers() throws Exception { TimeTraveller traveller = new TimeTraveller("James Cole"); Page<TimeTraveller> page = new PageImpl<>(Collections.singletonList(traveller)); given(projectService.getTimeTravellers(any())).willReturn(page); mvc.perform(get(apiPath) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.content[0].name", is(traveller.getName()))); } …
After you had the time traveller created in your API, the Postman output for http://localhost:8080/api/timetravellers?page=0&size=5&sort=id,asc will look like this:
{ "content": [ { "name": "James Cole", "id": 1 } ], "pageable": { "sort": { "sorted": true, "unsorted": false }, "pageSize": 5, "pageNumber": 0, "offset": 0, "paged": true, "unpaged": false }, "last": true, "totalElements": 1, "totalPages": 1, "first": true, "sort": { "sorted": true, "unsorted": false }, "numberOfElements": 1, "size": 5, "number": 0 }
Photo by Allef Vinicius on StockSnap
Spring Security allows us to use role-based control to restrict access to API resources. However,…
A custom annotation in Spring Boot tests is an easy and flexible way to provide…
Delegating user management to Keycloak allows us to better focus on meeting the business needs…
Swagger offers various methods to authorize requests to our Keycloak secured API. I'll show you…
Configuring our Spring Boot API to use Keycloak as an authentication and authorization server can…
Keycloak provides simple integration with Spring applications. As a result, we can easily configure our…
View Comments
Simply wish to say your article is as astounding. The clearness in your put up is just spectacular and i could think you are a professional on this subject. Well with your permission allow me to clutch your feed to stay up to date with impending post. Thanks a million and please carry on the rewarding work.
You have done a great Job. Well done!
I really liked this article. It solves my problem of creating and passing pagable object.