Fala, dev! Hoje vamos falar sobre um recurso essencial em qualquer API REST: paginação. Se você já teve que lidar com endpoints que retornam milhares de registros de uma vez, sabe o caos que isso pode causar. Vamos aprender a implementar paginação de forma elegante usando Spring Boot, e para deixar mais divertido, usaremos personagens do Senhor dos Anéis!
Por que paginar?
Imagine que você tem um endpoint que retorna todos os personagens da Terra Média. São centenas deles! Sem paginação, você teria problemas como:
- Performance ruim: Carregar tudo de uma vez consome muita memória
- Experiência do usuário prejudicada: Ninguém quer esperar 10 segundos para ver uma lista
- Desperdício de recursos: Muitas vezes o usuário só quer ver os primeiros resultados
O que você precisa ter pronto? 📋
Antes de começar, certifique-se de que você já tem:
-
Uma entidade JPA (no nosso caso,
Personagem)
@Entity
public class Personagem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nome;
private String raca;
private String arma;
// Construtores, getters e setters
}
-
Um Repository que estende
JpaRepository
public interface PersonagemRepository extends JpaRepository<Personagem, Long> {
// O método findAll(Pageable) já vem pronto!
}
- Um DTO de resposta (explicaremos o porquê logo abaixo!)
public class PersonagemResponseDTO {
private Long id;
private String nome;
private String raca;
private String arma;
// Construtores, getters e setters
}
🎯 Por que usar DTO em vez de retornar a Entidade direto?
Essa é uma dúvida muito comum! Parece mais trabalho criar uma classe DTO só pra retornar os mesmos dados, né? Mas tem bons motivos:
1. Segurança e Controle
- Sua entidade pode ter campos sensíveis (senha, dados internos) que você não quer expor na API
- Com DTO, você escolhe exatamente o que vai na resposta
2. Desacoplamento
- Mudanças no banco de dados (entidade) não quebram sua API
- Você pode ter uma estrutura de banco diferente da estrutura de resposta
- Exemplo: no banco você tem
nome_completo, mas na API retornanomeesobrenomeseparados3. Performance
- Você envia apenas os dados necessários pela rede
- Evita serializar informações desnecessárias ou relacionamentos complexos
4. Versionamento de API
- Fica mais fácil manter múltiplas versões da API (v1, v2) com DTOs diferentes usando a mesma entidade
Na prática: Sempre use DTOs em APIs REST profissionais. É uma das principais boas práticas de arquitetura! 💪
- Uma Service com método de conversão
@Service
public class PersonagemService {
private final PersonagemRepository repository;
public PersonagemService(PersonagemRepository repository) {
this.repository = repository;
}
private PersonagemResponseDTO entityParaResponseDTO(Personagem personagem) {
return new PersonagemResponseDTO(
personagem.getId(),
personagem.getNome(),
personagem.getRaca(),
personagem.getArma()
);
}
}
- Uma Controller REST
@RestController
@RequestMapping("/api/personagens")
public class PersonagemController {
private final PersonagemService service;
public PersonagemController(PersonagemService service) {
this.service = service;
}
}
Pronto! Com essa base, agora vamos adicionar a paginação! 🚀
Quero compartilhar o link do meu repositorio onde apliquei paginação:
https://github.com/eu-audrey/ApiNaruto
Mãos à obra! 🛠️
A boa notícia é que o Spring Data JPA torna isso super simples. Vamos precisar mexer apenas na Service e na Controller.
1. Implementando na Service Layer
public Page<PersonagemResponseDTO> listarPersonagensPaginado(Pageable pageable) {
Page<Personagem> paginasDePersonagens = personagemRepository.findAll(pageable);
return paginasDePersonagens.map(this::entityParaResponseDTO);
}
O que está acontecendo aqui?
- Recebemos um objeto
Pageableque contém todas as informações de paginação (página atual, tamanho, ordenação) - O método
findAll(pageable)do repository já faz toda a mágica de paginar no banco de dados - Usamos
map()para converter cada entidade em DTO, mantendo a estrutura de página
2. Criando o endpoint na Controller
@GetMapping
public ResponseEntity<List<PersonagemResponseDTO>> listarPersonagensPaginado(
@PageableDefault(page = 0, size = 10, sort = "nome", direction = Sort.Direction.ASC)
Pageable pageable) {
List<PersonagemResponseDTO> paginaDePersonagens =
personagemService.listarPersonagensPaginado(pageable).getContent();
return ResponseEntity.ok(paginaDePersonagens);
}
Destrinchando o código:
-
@PageableDefaultdefine valores padrão: primeira página, 10 itens por página, ordenado por nome -
getContent()extrai apenas a lista de itens da página - O Spring automaticamente reconhece parâmetros de query como
?page=0&size=10&sort=nome,asc
Como usar na prática?
Agora você pode fazer requisições como:
# Primeira página com 10 personagens
GET /api/personagens?page=0&size=10
# Segunda página com 5 personagens, ordenados por raça
GET /api/personagens?page=1&size=5&sort=raca,asc
# Ordenação por múltiplos campos
GET /api/personagens?page=0&size=10&sort=raca,asc&sort=nome,asc
Testando tudo! 🧪
Testando manualmente dentro da aplicação
A forma mais rápida de testar é usando sua própria aplicação. Veja como:
1. Popule seu banco com dados de teste
Crie uma classe DataLoader para inserir personagens:
@Component
public class PersonagemDataLoader implements CommandLineRunner {
private final PersonagemRepository repository;
public PersonagemDataLoader(PersonagemRepository repository) {
this.repository = repository;
}
@Override
public void run(String... args) throws Exception {
if (repository.count() == 0) {
repository.saveAll(Arrays.asList(
new Personagem(null, "Frodo", "Hobbit", "Bolseiro"),
new Personagem(null, "Aragorn", "Humano", "Montante"),
new Personagem(null, "Gandalf", "Maia", "Cajado"),
new Personagem(null, "Legolas", "Elfo", "Arco e Flecha"),
new Personagem(null, "Gimli", "Anão", "Machado"),
new Personagem(null, "Sam", "Hobbit", "Panelas"),
new Personagem(null, "Boromir", "Humano", "Espada e Escudo"),
new Personagem(null, "Merry", "Hobbit", "Espada"),
new Personagem(null, "Pippin", "Hobbit", "Espada"),
new Personagem(null, "Galadriel", "Elfa", "Magia"),
new Personagem(null, "Elrond", "Elfo", "Espada e Magia"),
new Personagem(null, "Saruman", "Maia", "Cajado")
));
}
}
}
2. Use ferramentas como Postman, Insomnia ou cURL
# Teste básico
curl http://localhost:8080/api/personagens
# Teste com paginação customizada
curl "http://localhost:8080/api/personagens?page=1&size=3&sort=nome,desc"
3. Adicione logs temporários na Service
public Page<PersonagemResponseDTO> listarPersonagensPaginado(Pageable pageable) {
System.out.println("Página solicitada: " + pageable.getPageNumber());
System.out.println("Tamanho da página: " + pageable.getPageSize());
System.out.println("Ordenação: " + pageable.getSort());
Page<Personagem> paginasDePersonagens = personagemRepository.findAll(pageable);
System.out.println("Total de elementos: " + paginasDePersonagens.getTotalElements());
System.out.println("Total de páginas: " + paginasDePersonagens.getTotalPages());
return paginasDePersonagens.map(this::entityParaResponseDTO);
}
4. Verifique o SQL gerado
Adicione isso no seu application.properties:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Você verá o SQL com LIMIT e OFFSET sendo gerado automaticamente!
Testando com Testes Unitários
@SpringBootTest
@AutoConfigureMockMvc
class PersonagemControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void deveRetornarPrimeiraPaginaComDezPersonagens() throws Exception {
mockMvc.perform(get("/api/personagens")
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(10));
}
@Test
void deveOrdenarPersonagensPorNome() throws Exception {
mockMvc.perform(get("/api/personagens")
.param("sort", "nome,asc"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].nome").value("Aragorn"));
}
}
📌 Nota sobre @Autowired: Nos testes, o
@Autowiredainda é usado porque o MockMvc precisa ser injetado pelo framework de testes. Mas no código de produção (Service, Controller, DataLoader), sempre prefira injeção por construtor!
Por que usar Injeção por Construtor? 🤔
Você deve ter notado que no DataLoader usamos injeção por construtor ao invés de @Autowired no campo. Isso é uma boa prática muito recomendada! Aqui está o porquê:
Vantagens da Injeção por Construtor
1. Imutabilidade com final
private final PersonagemRepository repository; // ✅ Pode ser final!
Com construtor, você pode marcar suas dependências como final, garantindo que elas nunca sejam alteradas após a criação do objeto. Com @Autowired no campo, isso não é possível.
2. Testabilidade
// Fica muito mais fácil testar!
PersonagemService service = new PersonagemService(mockRepository);
Você pode criar instâncias da classe manualmente em testes, sem precisar do Spring. Com @Autowired no campo, você fica preso ao container do Spring.
3. Dependências obrigatórias explícitas
O construtor deixa claro quais são as dependências necessárias. Se você tentar criar um objeto sem passar as dependências, o código nem compila! Com @Autowired, você só descobre em tempo de execução.
4. Menos "magia" do Spring
O código fica mais próximo do Java "puro", facilitando o entendimento por quem está aprendendo. Você vê claramente que está passando dependências, não precisa conhecer anotações específicas do Spring.
5. Código mais limpo
A partir do Spring 4.3, se você tem apenas um construtor, nem precisa da anotação @Autowired!
@Service
public class PersonagemService {
private final PersonagemRepository repository;
// Nem precisa de @Autowired aqui! O Spring injeta automaticamente
public PersonagemService(PersonagemRepository repository) {
this.repository = repository;
}
}
Quando usar @Autowired então?
Em pouquíssimos casos:
- Injeção em testes (como no
MockMvc) - Campos opcionais com
@Autowired(required = false) - Dependências circulares (mas isso geralmente indica problema de design!)
Resumo: Use injeção por construtor sempre que possível. É a forma mais profissional e recomendada pela comunidade Spring! 💪
Dicas importantes! 💡
- Defina limites razoáveis: Evite permitir que o cliente solicite 10.000 itens de uma vez
- Use indices no banco: Certifique-se de ter índices nas colunas usadas para ordenação
- Considere retornar metadados: Em APIs REST maduras, é comum retornar informações como total de páginas e total de elementos
Conclusão
Paginação com Spring Boot é realmente simples! Com poucas linhas de código, você tem uma API profissional e eficiente. O Spring Data faz o trabalho pesado, e você só precisa conectar as peças.
Agora sua aplicação está pronta para lidar com a Sociedade do Anel inteira, e até mesmo todos os habitantes da Terra Média! 🗡️
Gostou do conteúdo? Deixe seu ❤️ e compartilhe com outros devs que estão começando com Spring Boot!
Tem dúvidas? Comenta aqui embaixo que a gente se ajuda! 🚀

Top comments (0)