Capítulo 4: Paginação e Performance
Introdução
Problema: Você tem 100.000 produtos no banco. Vai exibir todos eles?
List<Product> products = productRepo.findAll(); // ❌ 100.000 objetos em memória!
Resultado:
- 💥 OutOfMemoryError
- 🐢 Aplicação muito lenta
- 😡 Usuários insatisfeitos
Este capítulo vai te ensinar a otimizar!
O que você vai aprender
- Paginação com
PageableePage<T> - Ordenação com
Sort - O problema N+1 e como resolvê-lo
- Projeções para performance
Duração: 1h
Paginação com Pageable
O Conceito
Ao invés de carregar tudo:
Página 1: [Produtos 1-20]
Página 2: [Produtos 21-40]
Página 3: [Produtos 41-60]
...
O usuário vê 20 resultados por vez, não 100.000!
Código: Repository com Paginação
public interface ProductRepository extends JpaRepository<Product, Long> {
// Método herdado
Page<Product> findAll(Pageable pageable);
// Seu método com paginação
Page<Product> findByCategory(String category, Pageable pageable);
// Com @Query
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
Page<Product> findCheap(@Param("maxPrice") Double maxPrice, Pageable pageable);
}
Pontos-chave:
- Parâmetro
Pageablepor último - Retorno
Page<T>(nãoList<T>)
Utilização: Criar um Pageable
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
// Página 0 (primeira página), 20 itens por página
Pageable pageable = PageRequest.of(0, 20);
Page<Product> page = productRepo.findAll(pageable);
// Informações da página
int totalPages = page.getTotalPages(); // Número total de páginas
long totalElements = page.getTotalElements(); // Número total de elementos
List<Product> products = page.getContent(); // Os 20 produtos da página
boolean hasNext = page.hasNext(); // Existe próxima página?
Exemplo Completo: API REST Paginada
Controller:
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductRepository productRepo;
public ProductController(ProductRepository productRepo) {
this.productRepo = productRepo;
}
@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
Pageable pageable = PageRequest.of(page, size);
return productRepo.findAll(pageable);
}
}
Requisição:
GET /api/products?page=0&size=20
Resposta JSON:
{
"content": [
{ "id": 1, "name": "Product 1" },
{ "id": 2, "name": "Product 2" },
...
],
"pageable": {
"pageNumber": 0,
"pageSize": 20
},
"totalPages": 500,
"totalElements": 10000,
"last": false,
"first": true
}
Ordenação com Sort
Ordenação Simples
import org.springframework.data.domain.Sort;
// Ordenar por preço ascendente
Sort sort = Sort.by("price").ascending();
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20, sort));
// Ordenar por preço descendente
Sort sort = Sort.by("price").descending();
Ordenação Múltipla
// Ordenar por categoria ASC, depois preço DESC
Sort sort = Sort.by("category").ascending()
.and(Sort.by("price").descending());
Pageable pageable = PageRequest.of(0, 20, sort);
Page<Product> page = productRepo.findAll(pageable);
Ordenação no Controller
@GetMapping
public Page<Product> getProducts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy,
@RequestParam(defaultValue = "ASC") String direction
) {
Sort sort = direction.equalsIgnoreCase("DESC")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return productRepo.findAll(pageable);
}
Requisição:
GET /api/products?page=0&size=20&sortBy=price&direction=DESC
O Problema N+1
O que é?
O problema de performance MAIS COMUM no JPA!
Exemplo do Problema
Entidades:
@Entity
public class Department {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees;
}
@Entity
public class Employee {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
}
Código problemático:
List<Department> departments = deptRepo.findAll(); // 1 consulta
for (Department dept : departments) {
List<Employee> employees = dept.getEmployees(); // N consultas!
System.out.println(dept.getName() + ": " + employees.size());
}
SQL executado:
-- 1 consulta
SELECT * FROM department;
-- N consultas (uma por departamento!)
SELECT * FROM employee WHERE department_id = 1;
SELECT * FROM employee WHERE department_id = 2;
SELECT * FROM employee WHERE department_id = 3;
...
SELECT * FROM employee WHERE department_id = N;
Se você tem 100 departamentos → 1 + 100 = 101 consultas! 💥
Solução 1: JOIN FETCH
@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
SQL executado:
-- Apenas 1 consulta!
SELECT d.*, e.*
FROM department d
LEFT JOIN employee e ON d.id = e.department_id;
✅ 1 consulta ao invés de 101!
Solução 2: @EntityGraph
@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();
Equivalente ao JOIN FETCH, mas mais simples!
Solução 3: Batch Size
Se JOIN FETCH não for possível:
@Entity
public class Department {
@OneToMany(mappedBy = "department")
@BatchSize(size = 10) // Carrega 10 departamentos por vez
private List<Employee> employees;
}
SQL executado:
SELECT * FROM department;
-- Ao invés de N consultas, apenas N/10!
SELECT * FROM employee WHERE department_id IN (1, 2, 3, ..., 10);
SELECT * FROM employee WHERE department_id IN (11, 12, 13, ..., 20);
...
Projeções para Performance
Problema
Você carrega TODA a entidade quando só precisa de 2 campos:
List<Product> products = productRepo.findAll();
for (Product p : products) {
System.out.println(p.getName() + ": " + p.getPrice());
// Você só usa name e price!
}
❌ Desperdício de memória e banda!
Solução 1: Interface Projection
Criar uma interface:
public interface ProductSummary {
String getName();
Double getPrice();
}
Repository:
List<ProductSummary> findAllBy();
Utilização:
List<ProductSummary> summaries = productRepo.findAllBy();
for (ProductSummary summary : summaries) {
System.out.println(summary.getName() + ": " + summary.getPrice());
}
✅ Carrega apenas name e price!
Solução 2: DTO com @Query
public class ProductDTO {
private String name;
private Double price;
public ProductDTO(String name, Double price) {
this.name = name;
this.price = price;
}
}
Repository:
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) FROM Product p")
List<ProductDTO> findAllDTOs();
Estratégias de Fetch
FetchType.LAZY (Recomendado)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
Vantagens:
- ✅ Carrega apenas se necessário
- ✅ Economiza memória
Desvantagem:
- ⚠️ Pode causar N+1
Solução: Use JOIN FETCH quando necessário
FetchType.EAGER (Evite)
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
Vantagens:
- ✅ Dados sempre disponíveis
Desvantagens:
- ❌ Carrega mesmo se não usado
- ❌ Pode carregar dados demais
Regra: Use LAZY por padrão!
Paginação + Ordenação + Performance: Exemplo Completo
public interface ProductRepository extends JpaRepository<Product, Long> {
// Paginação + Ordenação + Projeção
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) " +
"FROM Product p WHERE p.category = :category")
Page<ProductDTO> findByCategoryOptimized(
@Param("category") String category,
Pageable pageable
);
// Paginação + Ordenação + JOIN FETCH
@Query("SELECT p FROM Product p JOIN FETCH p.category")
Page<Product> findAllWithCategory(Pageable pageable);
}
Utilização:
// Página 0, 20 itens, ordenados por preço DESC
Pageable pageable = PageRequest.of(0, 20, Sort.by("price").descending());
// Carregamento otimizado
Page<ProductDTO> page = productRepo.findByCategoryOptimized("Electronics", pageable);
System.out.println("Total: " + page.getTotalElements());
System.out.println("Páginas: " + page.getTotalPages());
for (ProductDTO dto : page.getContent()) {
System.out.println(dto.getName() + ": " + dto.getPrice());
}
Dicas de Performance
1. Sempre Paginar Grandes Listas
// ❌ RUIM
List<Product> products = productRepo.findAll(); // Todos os produtos!
// ✅ BOM
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20));
2. Use Projeções
// ❌ Carrega a entidade inteira
List<Product> products = productRepo.findAll();
// ✅ Carrega apenas os campos necessários
List<ProductSummary> summaries = productRepo.findAllBy();
3. Evite o N+1
// ❌ Problema N+1
List<Employee> employees = empRepo.findAll();
employees.forEach(e -> System.out.println(e.getDepartment().getName()));
// ✅ JOIN FETCH
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
List<Employee> findAllWithDepartment();
4. Use LAZY por Padrão
// ✅ BOM
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// ❌ EVITE
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
5. Limite a Profundidade dos JOINs
// ❌ JOINs demais
@Query("SELECT e FROM Employee e " +
"JOIN FETCH e.department d " +
"JOIN FETCH d.company c " +
"JOIN FETCH c.industry i")
// ✅ Apenas o necessário
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
Monitoramento das Consultas
Ativar Logs SQL
application.properties:
# Exibir consultas SQL
spring.jpa.show-sql=true
# Formatar SQL (legível)
spring.jpa.properties.hibernate.format_sql=true
# Exibir parâmetros
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Estatísticas do Hibernate
spring.jpa.properties.hibernate.generate_statistics=true
Console:
Hibernate:
select
product0_.id as id1_0_,
product0_.name as name2_0_,
product0_.price as price3_0_
from
product product0_ limit ?
Resumo do Capítulo
Paginação
Pageable pageable = PageRequest.of(page, size, sort);
Page<Entity> page = repo.findAll(pageable);
Ordenação
Sort sort = Sort.by("field").ascending();
Sort multi = Sort.by("field1").and(Sort.by("field2").descending());
Problema N+1
// ❌ Problema
List<Entity> list = repo.findAll();
list.forEach(e -> e.getRelation());
// ✅ Solução
@Query("SELECT e FROM Entity e JOIN FETCH e.relation")
List<Entity> findAllWithRelation();
Projeções
// Interface
public interface Summary {
String getName();
}
// DTO
@Query("SELECT new DTO(e.name) FROM Entity e")
List<DTO> findDTOs();
Autoavaliação
- Como criar uma página com 20 itens ordenados por preço?
- O que é o problema N+1?
- Como resolvê-lo?
- Por que usar projeções?
Respostas:
PageRequest.of(0, 20, Sort.by("price").ascending())- 1 consulta principal + N consultas para as relações
- JOIN FETCH ou @EntityGraph
- Para carregar apenas os campos necessários
Parabéns! 🎉
Você concluiu toda a teoria do Módulo 6!
Agora você domina:
- ✅ 15+ palavras-chave de convenções
- ✅ Os 3 tipos de relações
- ✅ @Query (JPQL e SQL)
- ✅ Paginação e performance
Próximo passo: Praticar com os TPs!