Saltar al contenido principal

Capítulo 1: Preguntas de Entrevista — JPA & Base de Datos

Objetivo

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 consulta
  • getCustomer() 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​

EnfoqueViajes
Sin corrección (N+1)11 viajes para 10 pedidos
Con JOIN FETCH1 solo viaje para todo
Frase de entrevista

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?​

EstrategiaComportamientoValor por defecto para
LAZYCarga los datos relacionados solo cuando se accede a ellos@OneToMany, @ManyToMany
EAGERCarga los datos relacionados inmediatamente con la entidad@ManyToOne, @OneToOne
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;

@ManyToOne(fetch = FetchType.EAGER)
private Customer customer;
Trampa clásica

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.

Frase de entrevista

"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)
InterfazMétodos añadidos
CrudRepositorysave(), findById(), findAll(), deleteById(), count()
PagingAndSortingRepositoryfindAll(Pageable), findAll(Sort)
JpaRepositoryflush(), 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ónComportamiento
REQUIRED (por defecto)Se une a la transacción existente o crea una nueva
REQUIRES_NEWSiempre crea una nueva transacción (suspende la existente)
SUPPORTSSe une si existe, si no ejecuta sin transacción
NOT_SUPPORTEDSiempre ejecuta sin transacción
MANDATORYSe requiere una transacción existente, si no lanza excepción
NEVERSin transacción, si no lanza excepción
NESTEDTransacció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
}
}
Frase de entrevista

"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 = ?
}
Frase de entrevista

"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?​

EnfoqueCuándo usarlo
Query MethodsConsultas 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<>();
}
Trampa de entrevista

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 →