Capítulo 4: Paginación y Rendimiento
Introducción
Problema: ¿Tiene 100,000 productos en la base de datos. ¿Los muestra todos?
List<Product> products = productRepo.findAll(); // ❌ 100,000 objetos en memoria!
Resultado:
- 💥 OutOfMemoryError
- 🐢 Aplicación muy lenta
- 😡 Usuarios insatisfechos
¡Este capítulo le enseña a optimizar!
Lo que va a aprender
- Paginación con
PageableyPage<T> - Ordenamiento con
Sort - El problema N+1 y cómo resolverlo
- Proyecciones para el rendimiento
Duración: 1h
Paginación con Pageable
El Concepto
En lugar de cargar todo:
Página 1: [Productos 1-20]
Página 2: [Productos 21-40]
Página 3: [Productos 41-60]
...
El usuario ve 20 resultados a la vez, ¡no 100,000!
Código: Repositorio con Paginación
public interface ProductRepository extends JpaRepository<Product, Long> {
// Método heredado
Page<Product> findAll(Pageable pageable);
// Su método con paginación
Page<Product> findByCategory(String category, Pageable pageable);
// Con @Query
@Query("SELECT p FROM Product p WHERE p.price < :maxPrice")
Page<Product> findCheap(@Param("maxPrice") Double maxPrice, Pageable pageable);
}
Puntos clave:
- Parámetro
Pageableal final - Retorno
Page<T>(noList<T>)
Uso: Crear un Pageable
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
// Página 0 (primera página), 20 elementos por página
Pageable pageable = PageRequest.of(0, 20);
Page<Product> page = productRepo.findAll(pageable);
// Información de la 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(); // Los 20 productos de la página
boolean hasNext = page.hasNext(); // ¿Hay una página siguiente?
Ejemplo 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);
}
}
Petición:
GET /api/products?page=0&size=20
Respuesta 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
}
Ordenamiento con Sort
Ordenamiento Simple
import org.springframework.data.domain.Sort;
// Ordenar por precio ascendente
Sort sort = Sort.by("price").ascending();
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20, sort));
// Ordenar por precio descendente
Sort sort = Sort.by("price").descending();
Ordenamiento Múltiple
// Ordenar por categoría ASC, luego precio 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);
Ordenamiento en el 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);
}
Petición:
GET /api/products?page=0&size=20&sortBy=price&direction=DESC
El Problema N+1
¿Qué es?
¡El problema de rendimiento MÁS FRECUENTE en JPA!
Ejemplo del 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 ejecutado:
-- 1 consulta
SELECT * FROM department;
-- N consultas (¡una 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;
Si tiene 100 departamentos → 1 + 100 = 101 consultas! 💥
Solución 1: JOIN FETCH
@Query("SELECT d FROM Department d JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
SQL ejecutado:
-- ¡Solo 1 consulta!
SELECT d.*, e.*
FROM department d
LEFT JOIN employee e ON d.id = e.department_id;
✅ ¡1 consulta en lugar de 101!
Solución 2: @EntityGraph
@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();
¡Equivalente a JOIN FETCH pero más simple!
Solución 3: Batch Size
Si JOIN FETCH no es posible:
@Entity
public class Department {
@OneToMany(mappedBy = "department")
@BatchSize(size = 10) // Carga 10 departamentos a la vez
private List<Employee> employees;
}
SQL ejecutado:
SELECT * FROM department;
-- En lugar de N consultas, ¡solo N/10!
SELECT * FROM employee WHERE department_id IN (1, 2, 3, ..., 10);
SELECT * FROM employee WHERE department_id IN (11, 12, 13, ..., 20);
...
Proyecciones para el Rendimiento
Problema
Carga TODA la entidad cuando solo necesita 2 campos:
List<Product> products = productRepo.findAll();
for (Product p : products) {
System.out.println(p.getName() + ": " + p.getPrice());
// ¡Solo usa name y price!
}
❌ ¡Desperdicio de memoria y ancho de banda!
Solución 1: Interface Projection
Crear una interfaz:
public interface ProductSummary {
String getName();
Double getPrice();
}
Repositorio:
List<ProductSummary> findAllBy();
Uso:
List<ProductSummary> summaries = productRepo.findAllBy();
for (ProductSummary summary : summaries) {
System.out.println(summary.getName() + ": " + summary.getPrice());
}
✅ ¡Carga solo name y price!
Solución 2: DTO con @Query
public class ProductDTO {
private String name;
private Double price;
public ProductDTO(String name, Double price) {
this.name = name;
this.price = price;
}
}
Repositorio:
@Query("SELECT new com.example.dto.ProductDTO(p.name, p.price) FROM Product p")
List<ProductDTO> findAllDTOs();
Estrategias de Fetch
FetchType.LAZY (Recomendado)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
Ventajas:
- ✅ Carga solo si es necesario
- ✅ Ahorra memoria
Desventaja:
- ⚠️ Puede causar N+1
Solución: Utilizar JOIN FETCH cuando sea necesario
FetchType.EAGER (A Evitar)
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
Ventajas:
- ✅ Datos siempre disponibles
Desventajas:
- ❌ Carga incluso si no se usa
- ❌ Puede cargar demasiados datos
Regla: ¡Use LAZY por defecto!
Paginación + Ordenamiento + Rendimiento: Ejemplo Completo
public interface ProductRepository extends JpaRepository<Product, Long> {
// Paginación + Ordenamiento + Proyección
@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
);
// Paginación + Ordenamiento + JOIN FETCH
@Query("SELECT p FROM Product p JOIN FETCH p.category")
Page<Product> findAllWithCategory(Pageable pageable);
}
Uso:
// Página 0, 20 elementos, ordenados por precio DESC
Pageable pageable = PageRequest.of(0, 20, Sort.by("price").descending());
// Carga optimizada
Page<ProductDTO> page = productRepo.findByCategoryOptimized("Electronics", pageable);
System.out.println("Total: " + page.getTotalElements());
System.out.println("Pages: " + page.getTotalPages());
for (ProductDTO dto : page.getContent()) {
System.out.println(dto.getName() + ": " + dto.getPrice());
}
Consejos de Rendimiento
1. Siempre Paginar las Listas Grandes
// ❌ MALO
List<Product> products = productRepo.findAll(); // ¡Todos los productos!
// ✅ BUENO
Page<Product> page = productRepo.findAll(PageRequest.of(0, 20));
2. Utilizar Proyecciones
// ❌ Carga toda la entidad
List<Product> products = productRepo.findAll();
// ✅ Carga solo los campos necesarios
List<ProductSummary> summaries = productRepo.findAllBy();
3. Evitar el 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. Usar LAZY por Defecto
// ✅ BUENO
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// ❌ EVITAR
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
5. Limitar la Profundidad de los JOIN
// ❌ Demasiados JOIN
@Query("SELECT e FROM Employee e " +
"JOIN FETCH e.department d " +
"JOIN FETCH d.company c " +
"JOIN FETCH c.industry i")
// ✅ Solo lo necesario
@Query("SELECT e FROM Employee e JOIN FETCH e.department")
Monitoreo de Consultas
Activar los Logs SQL
application.properties:
# Mostrar las consultas SQL
spring.jpa.show-sql=true
# Formatear el SQL (legible)
spring.jpa.properties.hibernate.format_sql=true
# Mostrar los parámetros
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# Estadísticas de Hibernate
spring.jpa.properties.hibernate.generate_statistics=true
Consola:
Hibernate:
select
product0_.id as id1_0_,
product0_.name as name2_0_,
product0_.price as price3_0_
from
product product0_ limit ?
Resumen del Capítulo
Paginación
Pageable pageable = PageRequest.of(page, size, sort);
Page<Entity> page = repo.findAll(pageable);
Ordenamiento
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());
// ✅ Solución
@Query("SELECT e FROM Entity e JOIN FETCH e.relation")
List<Entity> findAllWithRelation();
Proyecciones
// Interfaz
public interface Summary {
String getName();
}
// DTO
@Query("SELECT new DTO(e.name) FROM Entity e")
List<DTO> findDTOs();
Autoevaluación
- ¿Cómo crear una página con 20 elementos ordenados por precio?
- ¿Qué es el problema N+1?
- ¿Cómo resolverlo?
- ¿Por qué usar proyecciones?
Respuestas:
PageRequest.of(0, 20, Sort.by("price").ascending())- 1 consulta principal + N consultas para las relaciones
- JOIN FETCH o @EntityGraph
- Para cargar solo los campos necesarios
¡Felicitaciones! 🎉
¡Ha terminado toda la teoría del Módulo 6!
Ahora domina:
- ✅ 15+ palabras clave de convenciones
- ✅ Los 3 tipos de relaciones
- ✅ @Query (JPQL y SQL)
- ✅ Paginación y rendimiento
Próximo paso: ¡Poner en práctica con los TPs!