DEV Community

Cover image for How to return paginated data in Spring Boot
Bruno Barbosa
Bruno Barbosa

Posted on

How to return paginated data in Spring Boot

At some point in your journey as a back end developer you will certainly come across a demand related to pagination.

We will often have a LOT of data persisted in databases and it would be impossible to present them all to the user. When we enter a virtual store, we are not presented with all the products at once, but rather in parts, using controllable filters and being able to advance pages to see the next products.

At first it may seem hopeless as you may try to go through the arduous path of counting elements, making divisions to determine the number of pages, filtering lists, etc.

However, we have a very practical, easy and quick-to-implement solution for us Java Spring developers, which is to use PagingAndSortingRepository passing a Pageable to the method.

These names may seem confusing at first, but don't worry, let's see what they are.

Before we start, I will present the base project that we will use. As always, I will point out that here I am not concerned with details such as exception handling, etc. so as not to lose focus on what we are really developing: paginated returns.

You can download the base code in the "basecode" branch of the following repository created for this tutorial: CLICK HERE

Base Project:

Let's create a small project (based on a previous project used in other posts) to persist and return student data. The data used will be very basic, being: registration number, name and last name.
Very simple so we don't lose focus.

The project was created in Java 21 with spring boot version 3.1.5

Dependencies used:
Lombok - to reduce boilerplate code
Mapstruct - facilitates object mapping
H2 - so we don't waste time configuring databases
JPA - since we will work with persistence
OpenAi - we will use a swagger so that you can run and use it without needing postman or another tool

Now I will present the main classes to you:

A) Student Entity - StudentEntity.class

@Entity
@Table(name = "students")
@Data
public class StudentEntity {

    @Id
    @Column(name = "registration")
    private Long registration;

    private String name;
    private String lastName;


}
Enter fullscreen mode Exit fullscreen mode

B) Request Class - StudentRequest.class

@Data
public class StudentRequest {

    private Long registration;
    private String name;
    private String lastName;

}
Enter fullscreen mode Exit fullscreen mode

C) Response Class - StudentResponse.class

@Data
public class StudentResponse {

    private Long registration;
    private String name;
    private String lastName;

}

Enter fullscreen mode Exit fullscreen mode

D) Controller

@RestController
@RequestMapping("students")
public class StudentController {

    @Autowired
    private StudentService studentService;

    @PostMapping
    ResponseEntity<StudentResponse> createStudent(@RequestBody StudentRequest studentRequest) {
        return ResponseEntity.ok().body(studentService.createStudent(studentRequest));
    }

    @GetMapping("all")
    ResponseEntity<List<StudentResponse>> getAllStudents() {
        return ResponseEntity.ok().body(studentService.getStudents());
    }

}
Enter fullscreen mode Exit fullscreen mode

Here we have two endpoints: a POST to create student, receiving an object from the request class already presented
and a GET to list all created students, without pagination or filters

We will now see our Repository class:

E) StudentRespository.class

public interface StudentRepository extends JpaRepository<StudentEntity, Long> {

}
Enter fullscreen mode Exit fullscreen mode

Very basic class that extends JpaRepository to use the standard save and findAll methods. We'll talk a little more about JpaRepository soon because it's important!

And finally, our Service class:

F) StudentServiceImpl.class - here is an Impl because it implements an interface.

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StudentRepository studentRepository;

    @Override
    public StudentResponse createStudent(StudentRequest studentRequest) {

        StudentEntity studentToSave = studentMapper.requestToEntity(studentRequest);

        StudentEntity savedStudent = studentRepository.save(studentToSave);

        return studentMapper.entityToResponse(savedStudent);
    }

    @Override
    public List<StudentResponse> getStudents() {
        List<StudentEntity> students = studentRepository.findAll();
        return studentMapper.entityToResponseList(students);
    }
}
Enter fullscreen mode Exit fullscreen mode

Only two methods, one to perform persistence and another to list the data in the database, without filters.

For those who are curious about using mapstruct, here is the class StudentMapper.class

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface StudentMapper {

    StudentResponse requestToResponse(StudentRequest studentRequest);
    StudentEntity requestToEntity(StudentRequest request);
    StudentResponse entityToResponse(StudentEntity entity);
    List<StudentResponse> entityToResponseList(List<StudentEntity> entities);

}
Enter fullscreen mode Exit fullscreen mode

I love mapstruct hahaha you will almost always find it and lombok in my posts.

Let's get to implementation!

Implementing Pagination

You can find this ready-made implementation in the 'main' branch or you can go directly to the 'pageable-implement' branch to see just the implementation code.

First Step: adjust repository to implement PagingAndSortingRepository

This interface provides pagination and ordering methods:

  • findAll(Pageable pageable) - returning a Page
  • findAll(Sort sort) - returning an Iterable In our case we don't need to make any modifications because when we extend JpaRepository, it already implements the PagingAndSortingRepository interface

Note that the first method used receives a Pageable, but what is a Pageable?

It is an interface with paging information, and the PageRequest class, which we will use to filter the information we want, implements this interface!
Click here to see the Pageable documentation!

Furthermore, it returns a Page, which is an interface for pagination as it is nothing more than a list of objects with some pagination information.

Adapt StudentResponse to map entity to response
As our repository will now return a Page we need to prepare our response to map entity to response because we don't want to directly expose our entities, right?
(note: I know that here my entity and response classes are the same, but normally they are not!
Second observation: we could use mapstruct for this too! But since not everyone uses it, I'll leave this solution that works for everyone)

so let's add the following method inside the StudentResponse.class class:

public static StudentResponse fromEntity(StudentEntity entity) {
        StudentResponse response = new StudentResponse();
        response.setRegistration(entity.getRegistration());
        response.setName(entity.getName());
        response.setLastName(entity.getLastName());
        return response;
    }
Enter fullscreen mode Exit fullscreen mode

this static method allows us to convert a StudentEntity to a StudentResponse.

Create the filtered search method in the Service Class - StudentServiceImpl.class

Now let's create a new method for filtered search:

@Override
    public Page<StudentResponse> getFilteredStudent(Integer page, Integer size, String orderBy, String direction) {
        PageRequest pageRequest = PageRequest.of(page, size, Sort.Direction.valueOf(direction), orderBy);
        Page<StudentEntity> foundStudents = studentRepository.findAll(pageRequest);
        return foundStudents.map(StudentResponse::fromEntity);
    }
Enter fullscreen mode Exit fullscreen mode

Let's analyze this method:

Firstly, it will return a Page, which means it will return a list of StudentResponse with pagination information, which I will show you all this information later, when we test the application!

This method receives a series of filters:
page - page we want to receive (in pagination it starts with 0)
size - number of elements per page
orderBy - which field are we going to order by?
direct - whether it is alphabetical/ascending (ASC) or descending (DESC)

To perform our paged search, our repository expects to receive a Pageable, to do so, we will instantiate a new object of the PageRequest class (this class is a Pageable) and we will pass the filter information we want to it.

Then we call the findAll(Pageable pageable) method of our repository passing our PageRequest (which is a Pageable) as an argument - remembering that we do not need to create this method in the repository because as it implements PagingAndSortingRepository (as it extends JpaRepository) this method is already ready for us!

and now let's take the return of this findAll, which is a Page and map it to Page. To do this, we will do a .map() using that static method that we created in our response class and return this to our controller.

Adding endpoint for pagination in the controller - StudentController.class

@GetMapping
    @Operation(summary = "list students using filter with pageable")
    ResponseEntity<Page<StudentResponse>> getFilteredStudent(@RequestParam(value = "page", defaultValue = "0") Integer page,
                                             @RequestParam(value = "size", defaultValue = "3") Integer size,
                                             @RequestParam(value = "orderBy", defaultValue = "lastName") String orderBy,
                                             @RequestParam(value = "direction", defaultValue = "ASC") String direction) {
        return ResponseEntity.ok().body(studentService.getFilteredStudent(page, size, orderBy, direction));
    }
Enter fullscreen mode Exit fullscreen mode

Here we are creating a GET endpoint for the paginated search receiving the filters via queryParams in the request uri. I have set default values ​​for each of the parameters in case they are not filled in.

We're ready to test! I'm going to test it with you using the swagger that was added in this little project, if you want to access it, just run the application and access:
http://localhost:8080/swagger-ui/index.html

First I will add some students using the create students endpoint:

Swagger image creating a new student

I will leave 7 students created for us to test.

Now let's call the endpoint that returns all students without pagination:

Swagger image returning all students without pagination

The return Json was as follows:

[
  {
    "registration": 1,
    "name": "Bruno",
    "lastName": "Affeldt"
  },
  {
    "registration": 2,
    "name": "Cassia",
    "lastName": "Cunha"
  },
  {
    "registration": 3,
    "name": "João",
    "lastName": "Pedro"
  },
  {
    "registration": 4,
    "name": "Gregorio",
    "lastName": "Oliveira"
  },
  {
    "registration": 5,
    "name": "Edgar",
    "lastName": "Rogerio"
  },
  {
    "registration": 6,
    "name": "Raphael",
    "lastName": "Santos"
  },
  {
    "registration": 7,
    "name": "Maria",
    "lastName": "Rosa"
  }
]
Enter fullscreen mode Exit fullscreen mode

A complete list of the 7 created students.

Let's now test our pagination!

I will use the following parameters:
page: 0
size: 3
orderBy: lastName
direction: ASC

Swagger image of the request to filtered students

And the return Json was as follows:

{
  "content": [
    {
      "registration": 1,
      "name": "Bruno",
      "lastName": "Affeldt"
    },
    {
      "registration": 2,
      "name": "Cassia",
      "lastName": "Cunha"
    },
    {
      "registration": 4,
      "name": "Gregorio",
      "lastName": "Oliveira"
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 3,
    "sort": {
      "empty": false,
      "sorted": true,
      "unsorted": false
    },
    "offset": 0,
    "unpaged": false,
    "paged": true
  },
  "last": false,
  "totalPages": 3,
  "totalElements": 7,
  "size": 3,
  "number": 0,
  "sort": {
    "empty": false,
    "sorted": true,
    "unsorted": false
  },
  "numberOfElements": 3,
  "first": true,
  "empty": false
}
Enter fullscreen mode Exit fullscreen mode

This return Json has various pagination information that the front end can use. If he just wants the elements, just get what's in the content field
but notice that we have a lot of other information, such as, for example, if there was ordering, how it was ordered, what is the total number of pages, which page it is on, how many elements there are in total and much other information.

Yes, the information is in English and you, the developer, should already be familiar with it. However, there is a way to translate terms. However, that is for another post.

I hope you understood and liked this implementation. Suggestions are welcome and any questions just comment here.
Thank you for reading.

Top comments (0)