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.
What we are going to build
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:
- the
TimeTraveller
entity withid
andname
properties, TimeTravellerRepository
,TimeTravellerService
for fetching data,TimeTravellerController
for providing the endpoint.
Requirements
The following fragment of the pom.xml
file shows dependencies used in the project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 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> … |
Enable pagination
To allow paginating requested data we have to make sure that TimeTravellerRepository
extends the PagingAndSortingRepository interface:
1 2 3 4 5 |
//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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// 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); } |
Get paginated results
We are going to handle the following request:
1 |
http://localhost:8080/api/timetravellers?page=0&size=5&sort=id,asc |
The controller endpoint looks straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
{ "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
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.