Pular para o conteúdo principal

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​

  1. Paginação com Pageable e Page<T>
  2. Ordenação com Sort
  3. O problema N+1 e como resolvê-lo
  4. 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 Pageable por último
  • Retorno Page<T> (não List<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​

  1. Como criar uma página com 20 itens ordenados por preço?
  2. O que é o problema N+1?
  3. Como resolvê-lo?
  4. Por que usar projeções?

Respostas:

  1. PageRequest.of(0, 20, Sort.by("price").ascending())
  2. 1 consulta principal + N consultas para as relações
  3. JOIN FETCH ou @EntityGraph
  4. 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!


➡️ Começar TP6.1: Convenções Avançadas

⬅️ Capítulo anterior: @Query

⬅️ Voltar ao Módulo 6