Capítulo 1: Preguntas de Entrevista — JPA & Base de Datos
Este capítulo cubre las preguntas más frecuentes sobre JPA, Hibernate, las relaciones entre entidades y los problemas de rendimiento en bases de datos. Cada pregunta incluye una respuesta simplificada y una frase clave para decir en la entrevista.
Q1. ¿Qué es el problema N+1 en JPA?
La analogía del restaurante
Imagine que usted le pide al camarero en un restaurante:
"Tráigame los 10 pedidos"
Luego, para cada pedido, vuelve a preguntar:
"¿Y quién es el cliente de este pedido?"
No hace una sola gran solicitud. Hace:
- 1 solicitud para los pedidos
- luego 10 pequeñas solicitudes para los clientes
Eso genera muchos viajes innecesarios.
¿Qué es N+1 concretamente?
1 consulta para la lista principal
+ N consultas para los datos relacionados
────────────────────────────────
= N + 1 consultas en total
Si N = 10 pedidos:
SELECT * FROM orders; -- 1 consulta
SELECT * FROM customers WHERE id = 1; -- consulta 2
SELECT * FROM customers WHERE id = 2; -- consulta 3
SELECT * FROM customers WHERE id = 3; -- consulta 4
...
SELECT * FROM customers WHERE id = 10; -- consulta 11
Total = 11 consultas, cuando una sola habría sido suficiente.
¿Por qué ocurre?
Porque a menudo la relación order.customer se carga con lazy loading. JPA no carga el cliente al principio — espera a que usted haga order.getCustomer().
El código que provoca el N+1
List<Order> orders = orderRepository.findAll();
for (Order o : orders) {
System.out.println(o.getCustomer().getName());
// ↑ ¡CADA llamada dispara una consulta SQL!
}
Aquí:
findAll()carga los pedidos → 1 consultagetCustomer()dispara 1 consulta por pedido → N consultas
La solución: JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomers();
JPA hace una sola consulta que trae tanto los pedidos como sus clientes.
Alternativa: @EntityGraph
@EntityGraph(attributePaths = {"customer"})
List<Order> findAll();
Alternativa: @BatchSize
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 50)
private Customer customer;
}
En lugar de hacer N consultas, JPA hace N/50 (carga los clientes en lotes de 50).
Imagen mental simple
| Enfoque | Viajes |
|---|---|
| Sin corrección (N+1) | 11 viajes para 10 pedidos |
| Con JOIN FETCH | 1 solo viaje para todo |
EN: "The N+1 problem happens when JPA fires one query per related entity. I fix it with JOIN FETCH or @EntityGraph to load everything in a single query."
ES: "El problema N+1 ocurre cuando JPA ejecuta una consulta inicial y luego una consulta adicional por cada entidad relacionada. Lo corrijo con JOIN FETCH o @EntityGraph para cargar todo en una sola consulta."
Q2. ¿Cuál es la diferencia entre Lazy Loading y Eager Loading?
| Estrategia | Comportamiento | Valor por defecto para |
|---|---|---|
| LAZY | Carga los datos relacionados solo cuando se accede a ellos | @OneToMany, @ManyToMany |
| EAGER | Carga los datos relacionados inmediatamente con la entidad | @ManyToOne, @OneToOne |
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
EAGER en todas partes = catástrofe de rendimiento. Si cada entidad carga sus 5 relaciones en EAGER, una simple consulta puede disparar cientos de consultas en cascada.
Buena práctica: Todo en LAZY + JOIN FETCH cuando sea necesario.
"Por defecto configuro todo en LAZY loading y uso JOIN FETCH o EntityGraph cuando necesito cargar asociaciones específicas de forma anticipada para un caso de uso."
Q3. ¿Cuál es la diferencia entre JpaRepository, CrudRepository y PagingAndSortingRepository?
JpaRepository (el más completo)
↑ hereda de
PagingAndSortingRepository (paginación + ordenamiento)
↑ hereda de
CrudRepository (CRUD básico)
↑ hereda de
Repository (interfaz marcador)
| Interfaz | Métodos añadidos |
|---|---|
| CrudRepository | save(), findById(), findAll(), deleteById(), count() |
| PagingAndSortingRepository | findAll(Pageable), findAll(Sort) |
| JpaRepository | flush(), saveAndFlush(), deleteInBatch(), retorna List<T> en vez de Iterable<T> |
En la práctica, casi siempre se utiliza JpaRepository.
Q4. Explique las estrategias de propagación de @Transactional
| Propagación | Comportamiento |
|---|---|
REQUIRED (por defecto) | Se une a la transacción existente o crea una nueva |
REQUIRES_NEW | Siempre crea una nueva transacción (suspende la existente) |
SUPPORTS | Se une si existe, si no ejecuta sin transacción |
NOT_SUPPORTED | Siempre ejecuta sin transacción |
MANDATORY | Se requiere una transacción existente, si no lanza excepción |
NEVER | Sin transacción, si no lanza excepción |
NESTED | Transacción anidada (savepoint) |
Caso de uso concreto de REQUIRES_NEW
@Transactional
public void processPayment(Order order) {
paymentGateway.charge(order); // Transacción principal
auditService.logPayment(order); // Si falla, el pago debe persistir igual
}
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPayment(Order order) {
// Nueva transacción independiente
// Incluso si esta transacción falla, el pago se guarda
}
}
"Uso REQUIRED por defecto. Cambio a REQUIRES_NEW cuando necesito una transacción independiente, como el registro de auditoría que debe persistir independientemente del resultado de la transacción principal."
Q5. Diferencia entre @Transactional(readOnly = true) y @Transactional
@Transactional(readOnly = true) // Lectura: Hibernate desactiva el dirty checking
public List<User> findAll() { ... }
@Transactional // Escritura: commit si tiene éxito, rollback si hay excepción
public User create(UserDto dto) { ... }
Buena práctica: Poner @Transactional(readOnly = true) a nivel de clase, y @Transactional en los métodos de escritura:
@Service
@Transactional(readOnly = true)
public class UserService {
public List<User> findAll() { ... } // Hereda readOnly = true
public User findById(Long id) { ... } // Hereda readOnly = true
@Transactional // Override: escritura
public User create(UserDto dto) { ... }
@Transactional // Override: escritura
public void delete(Long id) { ... }
}
Q6. ¿Cómo implementar la paginación en Spring Data JPA?
// Repository — no hay que hacer nada, JpaRepository soporta Pageable nativamente
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findByRole(Role role, Pageable pageable);
}
// Controller
@GetMapping("/users")
public Page<User> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "name") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
return userRepository.findAll(pageable);
}
El Page<T> retornado contiene: contenido, número de página, tamaño, total de elementos, número de páginas.
Q7. ¿Qué son los Query Methods en Spring Data JPA?
Spring Data genera automáticamente las consultas SQL a partir del nombre del método:
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByName(String name);
// → SELECT * FROM users WHERE name = ?
List<User> findByNameAndEmail(String name, String email);
// → SELECT * FROM users WHERE name = ? AND email = ?
List<User> findByAgeBetween(int min, int max);
// → SELECT * FROM users WHERE age BETWEEN ? AND ?
List<User> findByNameContainingIgnoreCase(String keyword);
// → SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%keyword%')
Optional<User> findByEmail(String email);
// → SELECT * FROM users WHERE email = ? LIMIT 1
long countByRole(Role role);
// → SELECT COUNT(*) FROM users WHERE role = ?
boolean existsByEmail(String email);
// → SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM users WHERE email = ?
}
"Spring Data JPA deriva las consultas a partir de los nombres de los métodos. Para consultas complejas, utilizo @Query con JPQL o SQL nativo."
Q8. ¿Cuándo usar @Query vs los Query Methods?
| Enfoque | Cuándo usarlo |
|---|---|
| Query Methods | Consultas simples (1-2 condiciones) |
| @Query (JPQL) | Consultas complejas, JOIN, agregaciones |
| @Query (native) | Consultas SQL específicas del SGBD, rendimiento crítico |
// JPQL
@Query("SELECT u FROM User u WHERE u.department.name = :dept AND u.active = true")
List<User> findActiveByDepartment(@Param("dept") String dept);
// SQL nativo
@Query(value = "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'",
nativeQuery = true)
List<User> findRecentUsers();
Q9. ¿Cómo gestionar las relaciones entre entidades?
@ManyToOne / @OneToMany (la más frecuente)
@Entity
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
@Entity
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
}
@ManyToMany
@Entity
public class Student {
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
}
Siempre use Set para @ManyToMany, nunca List — de lo contrario Hibernate puede generar consultas ineficientes (elimina todo y luego inserta de nuevo).
⬅️ Volver al Módulo 18 | Capítulo 2: Ejercicios de Refactoring →