Categories: Spring Boot

How to add pagination to a Spring Boot app

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 with id and name 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:

// 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:

//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);
    }

Get paginated results

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

little_pinecone

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.

Recent Posts

Simplify the management of user roles in Spring Boot

Spring Security allows us to use role-based control to restrict access to API resources. However,…

3 years ago

Create a custom annotation to configure Spring Boot tests

A custom annotation in Spring Boot tests is an easy and flexible way to provide…

3 years ago

Keycloak with Spring Boot #4 – Simple guide for roles and authorities

Delegating user management to Keycloak allows us to better focus on meeting the business needs…

3 years ago

Keycloak with Spring Boot #3 – How to authorize requests in Swagger UI

Swagger offers various methods to authorize requests to our Keycloak secured API. I'll show you…

3 years ago

Keycloak with Spring Boot #2 – Spring Security instead of Keycloak in tests

Configuring our Spring Boot API to use Keycloak as an authentication and authorization server can…

3 years ago

Keycloak with Spring Boot #1 – Configure Spring Security with Keycloak

Keycloak provides simple integration with Spring applications. As a result, we can easily configure our…

3 years ago