DEV Community

Cover image for Como retornar dados paginados no Spring Boot
Bruno Barbosa
Bruno Barbosa

Posted on

Como retornar dados paginados no Spring Boot

Em algum momento da sua jornada de desenvolvedor back end você com certeza irá se deparar com uma demanda relacionada à paginação.

Muitas vezes teremos MUITOS dados persistidos em bancos de dados e seria inviável apresentar todos para o usuário. Quando entramos em uma loja virtual, não nos é apresentado todos os produtos de uma vez só, mas sim em partes, utilizando filtros controláveis e podendo avançar páginas para vermos os próximos produtos.

No início pode parecer desesperador pois você pode tentar ir pelo caminho árduo de contar elementos, fazer divisões para determinar o número de páginas, filtrar listas, etc.

Porém temos uma solução muito prática, fácil e de rápida implementação para nós desenvolvedores Java Spring, que é utilizar PagingAndSortingRepository passando para o método um Pageable.

Esses nomes podem parecer confusos no início, mas fique tranquilo pois vamos ver o que são.

Antes de começarmos, vou apresentar o projeto base que iremos utilizar. Como sempre, vou salientar que aqui não estou preocupado com detalhes como tratamento de exceções, etc para não perdermos o foco do que realmente estamos desenvolvendo: retornos paginados.

Você pode baixar o código base na branch "basecode" do seguinte repositório criado para este tutorial: CLICA AQUI

Projeto Base:

Vamos criar um pequeno projeto (baseado em projeto anterior utilizado em outras postagens) para persistir e retornar dados de estudantes. Os dados utilizados serão bem básicos, sendo: matrícula, nome e último nome.
Bem simples para não perdermos o foco.

O projeto foi criado em Java 21 com spring boot versão 3.1.5

Dependências utilizadas:
Lombok - para reduzir código boilerplate
Mapstruct - facilita mapeamento de objetos
H2 - para não perdermos tempo configurando bancos de dados
JPA - já que trabalharemos com persistência
OpenAi - vamos utilizar um swagger para que você possa rodar e utilizar sem precisar de postman ou outra ferramenta

Agora vou apresentar as principais classes para vocês:

A) Entidade de Estudante - 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) Classe de Request - StudentRequest.class

@Data
public class StudentRequest {

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

}
Enter fullscreen mode Exit fullscreen mode

C) Classe de Response - StudentResponse.class

@Data
public class StudentResponse {

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

}

Enter fullscreen mode Exit fullscreen mode

D) Controller - por onde chamam nossos endpoints

@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

Aqui temos dois endpoints: um POST para criar estudante, recebendo um objeto da classe de request já apresentada
e um GET para listar todos os estudantes criados, sem paginação ou filtros

Veremos agora nossa classe de Repository:

E) StudentRespository.class

public interface StudentRepository extends JpaRepository<StudentEntity, Long> {

}
Enter fullscreen mode Exit fullscreen mode

Classe bem básica que extende JpaRepository para utilizarmos os métodos save e findAll padrões. Falaremos um pouco mais sobre JpaRepository em breve pois ela é importante!

E, por último, nossa classe Service:

F) StudentServiceImpl.class - aqui é uma Impl pois ela implementa uma 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

Apenas dois métodos, um para realizar persistência e outro para fazer a listagem dos dados no banco de dados, sem filtros.

Para quem está curioso sobre a utilização do mapstruct, aqui está a classe 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

eu amo mapstruct hahaha vocês quase sempre encontrarão ele e o lombok nas minhas postagens.

Vamos à implementação!

Implementando Paginação

essa implementação pronta você pode encontrar na branch 'main' ou pode ir direto na branch 'pageable-implement' para ver apenas o código da implementação.

Primeiro Passo: ajustar repository para implementar PagingAndSortingRepository

Essa interface provê métodos de paginação e ordenação:

  • findAll(Pageable pageable) - retornando um Page
  • findAll(Sort sort) - retornando um Iterable No nosso caso nós não precisamos fazer nenhuma modificação pois quando extendemos JpaRepository, ele já implementa a interface PagingAndSortingRepository

Perceba que o primeiro método utilizado recebe um Pageable, mas o que é um Pageable?

É uma interface com informações de paginação, e a classe PageRequest, que vamos utilizar para filtrar as informações que queremos implementa essa interface!
Clica aqui para ver a documentação de Pageable!

Além disso, ela retorna um Page, que é uma interface para paginação pois ele nada mais é do que uma lista de objetos com algumas informações da paginação.

Adaptar StudentResponse para mapear a entidade
Como nosso repository agora irá retornar um Page nós precisamos preparar nosso response para mapear entidade para response pois não queremos expor diretamente nossas entidades, não é mesmo?
(obs: sei que aqui minha classe de entidade e de response são iguais, mas normalmente não são!
Segunda observação: poderíamos utilizar o mapstruct para isso também! Mas como nem todos utilizam, vou deixar essa solução que funciona para todos)

então vamos adicionar o seguinte método dentro da classe StudentResponse.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

esse método estático nos permite converter uma StudentEntity em um StudentResponse.

Criar o método de busca filtrada na Classe de Service - StudentServiceImpl.class

Agora vamos criar um método novo para busca filtrada:

@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

Vamos analisar esse método:

Primeiramente, ele vai devolver um Page isso quer dizer que ele vai retornar uma lista de StudentResponse com informações de paginação, que vou te mostrar todas essas informações depois, quando testarmos a aplicação!

Esse método recebe uma série de filtros:
page - página que queremos receber (na paginação começa com 0)
size - número de elementos por página
orderBy - por qual campo vamos ordenar
direct - se é alfabético/crescente (ASC) ou decrescente (DESC)

Para realizarmos nossa busca paginada, nosso repository espera receber um Pageable, para tal, vamos instanciar um novo objeto da classe PageRequest (essa classe é um Pageable) e vamos passar para ela as informações de filtro que queremos.

Em seguida chamamos o método findAll(Pageable pageable) do nosso repository passando como argumento nosso PageRequest (que é um Pageable) - lembrando que não precisamos criar esse método no repository pois como ele implementa PagingAndSortingRepository (pois extends JpaRepository) esse método já está pronto pra nós!

e agora vamos pegar o retorno desse findAll, que é um Page e mapear para Page. Para isso vamos fazer um .map() utilizando aquele método estático que criamos na nossa classe de response e retornar isso para nosso controller.

Adicionando endpoint para paginação no 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

Aqui estamos criando um endpoint GET para a busca paginada recebendo os filtros por queryParams na uri da requisição. Cada um dos parâmetros coloquei valores padrão para caso não venham preenchidos.

Estamos prontos para testar! Vou testar com vocês utilizando o swagger que foi adicionado nesse projetinho, caso vocês queiram acessar, basta rodar a aplicação e acessar:
http://localhost:8080/swagger-ui/index.html

Primeiramente vou adicionar alguns estudantes utilizando o endpoint de criar estudantes:

Swagger image creating a new student

Vou deixar criados 7 estudantes para testarmos.

Agora vamos chamar o endpoint que retorna todos os estudantes sem paginação:

Swagger image returning all student without pagination

O Json de retorno foi o seguinte:

[
  {
    "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

Uma lista completa dos 7 estudantes criados.

Vamos agora testar nossa paginação!

Vou utilizar os seguintes parâmetros:
page: 0
size: 3
orderBy: lastName
direction: ASC

Swagger image of the request to filtered students

E o Json de retorno foi o seguinte:

{
  "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

Esse Json de retorno possui várias informações da paginação que o front end pode utilizar. Caso ele queira apenas os elementos, basta ele pegar o que tem no campo content
mas perceba que temos várias outras informações, como, por exemplo, se houve ordenação, como foi ordenado, qual o total de páginas, em qual página está, quantos elementos tem no total e muitas outras informações.

Sim, as informações estão em inglês e você desenvolvedor já deveria estar familiarizado com isso. Porém, tem como traduzir termos. Porém, isso fica para outra postagem.

Espero que você tenha entendido e gostado dessa implementação. Sugestões são bem vindas e quaisquer dúvidas é só comentar aqui.
Obrigado por ter lido.

Top comments (0)