Capítulo 2: Relaciones entre Entidades
Introducción
En el mundo real, los datos están relacionados:
- Un Cliente realiza varios Pedidos
- Un Departamento contiene varios Empleados
- Un Estudiante cursa varias Asignaturas
¡Este capítulo le enseña a modelar estas relaciones con JPA!
Lo que va a aprender
- Los 3 tipos de relaciones (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
- Tipos de fetch (LAZY vs EAGER)
- Operaciones en cascada
- Relaciones bidireccionales
Duración: 1h30
Problema Sin Relaciones
Enfoque Ingenuo (Incorrecto)
@Entity
public class Employee {
@Id
private Long id;
private String name;
private Long departmentId; // ❌ ¡Solo un ID!
}
@Entity
public class Department {
@Id
private Long id;
private String name;
}
Problemas:
// Para recuperar el departamento de un empleado:
Employee emp = empRepo.findById(1L);
Department dept = deptRepo.findById(emp.getDepartmentId()); // ¡2 consultas!
// Para recuperar todos los empleados de un departamento:
List<Employee> emps = empRepo.findByDepartmentId(deptId); // ¡Sin relación!
Solución: Relaciones JPA
Con Relaciones (Correcto)
@Entity
public class Employee {
@Id
private Long id;
private String name;
@ManyToOne // ✅ ¡Relación JPA!
private Department department;
}
@Entity
public class Department {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "department") // ✅ ¡Relación inversa!
private List<Employee> employees;
}
Ventajas:
// Departamento de un empleado - ¡FÁCIL!
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment(); // ¡Acceso directo!
// Empleados de un departamento - ¡FÁCIL!
Department dept = deptRepo.findById(1L);
List<Employee> emps = dept.getEmployees(); // ¡Acceso directo!
Tipo 1: @OneToOne (1-1)
¿Qué es?
Una entidad relacionada con UNA SOLA otra entidad.
Ejemplos:
- User ↔ UserProfile (un usuario tiene un perfil)
- Person ↔ Passport (una persona tiene un pasaporte)
- Employee ↔ ParkingSpot (un empleado tiene una plaza de estacionamiento)
Código: User y UserProfile
Entidad User:
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// Relación OneToOne
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id") // Clave foránea en la tabla users
private UserProfile profile;
// Constructores, getters, setters
}
Entidad UserProfile:
@Entity
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String bio;
private String avatar;
// Relación inversa (opcional)
@OneToOne(mappedBy = "profile")
private User user;
// Constructores, getters, setters
}
Uso
// Crear
User user = new User("john", "john@email.com");
UserProfile profile = new UserProfile("Bio de John", "avatar.jpg");
user.setProfile(profile);
userRepo.save(user); // El perfil se guarda automáticamente (cascade)
// Leer
User user = userRepo.findById(1L);
UserProfile profile = user.getProfile();
System.out.println(profile.getBio());
Tipo 2: @ManyToOne y @OneToMany (N-1 / 1-N)
¿Qué es?
Varias entidades relacionadas con UNA entidad.
Ejemplos:
- Department ↔ Employees (un departamento tiene varios empleados)
- Author ↔ Books (un autor escribe varios libros)
- Category ↔ Products (una categoría contiene varios productos)
Código: Department y Employee
Lado Many (Employee):
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
// MUCHOS empleados pertenecen a UN departamento
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id") // FK en tabla employee
private Department department;
// Constructores
public Employee() {}
public Employee(String name, Double salary) {
this.name = name;
this.salary = salary;
}
// Getters, setters
}
Lado One (Department):
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// UN departamento tiene MUCHOS empleados
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();
// Constructores
public Department() {}
public Department(String name) {
this.name = name;
}
// Métodos helper
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // Enlace bidireccional
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
// Getters, setters
}
Uso
// Crear un departamento con empleados
Department it = new Department("IT");
it.addEmployee(new Employee("Alice", 50000.0));
it.addEmployee(new Employee("Bob", 60000.0));
deptRepo.save(it); // Empleados guardados automáticamente
// Leer los empleados de un departamento
Department dept = deptRepo.findById(1L);
List<Employee> employees = dept.getEmployees();
employees.forEach(emp -> System.out.println(emp.getName()));
// Leer el departamento de un empleado
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment();
System.out.println(dept.getName());
Tipo 3: @ManyToMany (N-N)
¿Qué es?
Varias entidades relacionadas con VARIAS entidades.
Ejemplos:
- Students ↔ Courses (un estudiante cursa varias asignaturas, una asignatura tiene varios estudiantes)
- Books ↔ Authors (un libro tiene varios autores, un autor escribe varios libros)
- Products ↔ Tags (un producto tiene varias etiquetas, una etiqueta en varios productos)
¡Requiere una tabla de unión!
Código: Student y Course
Entidad Student:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// MUCHOS estudiantes cursan MUCHOS cursos
@ManyToMany
@JoinTable(
name = "student_course", // Tabla de unión
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
// Métodos helper
public void enrollCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
public void dropCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
// Constructores, getters, setters
}
Entidad Course:
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// Relación inversa
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();
// Constructores, getters, setters
}
Tabla generada:
CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id)
);
Uso
// Crear
Student alice = new Student("Alice");
Course java = new Course("Java Programming");
Course spring = new Course("Spring Boot");
alice.enrollCourse(java);
alice.enrollCourse(spring);
studentRepo.save(alice);
// Leer los cursos de un estudiante
Student student = studentRepo.findById(1L);
List<Course> courses = student.getCourses();
// Leer los estudiantes de un curso
Course course = courseRepo.findById(1L);
List<Student> students = course.getStudents();
Fetch Types: LAZY vs EAGER
El Problema
¿Cuándo cargar las entidades relacionadas?
Employee emp = empRepo.findById(1L);
// Pregunta: ¿El Department ya está cargado?
EAGER (Carga Inmediata)
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
Comportamiento:
Employee emp = empRepo.findById(1L);
// ¡Department ya está cargado!
Department dept = emp.getDepartment(); // No hay consulta adicional
SQL generado:
SELECT e.*, d.*
FROM employee e
LEFT JOIN department d ON e.department_id = d.id
WHERE e.id = 1;
Ventajas:
- ✅ Datos disponibles inmediatamente
- ✅ No hay consulta adicional
Desventajas:
- ❌ Carga datos aunque no se usen
- ❌ Puede ser lento si hay muchas relaciones
LAZY (Carga Diferida) - Recomendado
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
Comportamiento:
Employee emp = empRepo.findById(1L);
// Department TODAVÍA no está cargado (proxy)
Department dept = emp.getDepartment(); // Consulta AHORA
SQL generado:
-- 1ª consulta
SELECT * FROM employee WHERE id = 1;
-- 2ª consulta (solo si se accede a department)
SELECT * FROM department WHERE id = ?;
Ventajas:
- ✅ Solo carga si es necesario
- ✅ Mejor rendimiento general
Desventajas:
- ⚠️ Consulta adicional si se accede
- ⚠️ Puede causar el problema N+1
¿Cuándo Usar Cada Uno?
// ✅ LAZY para @OneToMany (lista)
@OneToMany(fetch = FetchType.LAZY) // Por defecto
private List<Employee> employees;
// ✅ LAZY para @ManyToMany (lista)
@ManyToMany(fetch = FetchType.LAZY) // Por defecto
private List<Course> courses;
// ⚠️ LAZY para @ManyToOne (recomendado)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
// ⚠️ EAGER solo si SIEMPRE se usa
@ManyToOne(fetch = FetchType.EAGER)
private Department department; // Solo si siempre es necesario
Regla de oro: ¡Use LAZY por defecto!
Cascade Operations
El Problema
¿Qué sucede cuando guardo/elimino una entidad con relaciones?
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// Pregunta: ¿El empleado también se guarda?
Tipos de Cascade
public enum CascadeType {
PERSIST, // Guardado en cascada
MERGE, // Actualización en cascada
REMOVE, // Eliminación en cascada
REFRESH, // Refresco en cascada
DETACH, // Desvinculación en cascada
ALL // Todas las operaciones
}
Cascade.PERSIST (Guardar)
@OneToMany(mappedBy = "department", cascade = CascadeType.PERSIST)
private List<Employee> employees;
Comportamiento:
Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// ✅ ¡Employee guardado automáticamente!
Cascade.REMOVE (Eliminar)
@OneToMany(mappedBy = "department", cascade = CascadeType.REMOVE)
private List<Employee> employees;
Comportamiento:
deptRepo.deleteById(1L);
// ⚠️ ¡TODOS los empleados del departamento son eliminados!
Atención: ¡Úselo con precaución!
Cascade.ALL (Todo)
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees;
Comportamiento:
// Guardado, actualización, eliminación en cascada
deptRepo.save(dept); // Empleados guardados
deptRepo.delete(dept); // Empleados eliminados
Recomendaciones
// ✅ CORRECTO - El padre es dueño de los hijos
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items; // OrderItems pertenecen a Order
// ⚠️ ATENCIÓN - Entidades compartidas
@ManyToOne(cascade = CascadeType.PERSIST)
private Department department; // ¡NO usar REMOVE!
// ❌ PELIGROSO
@ManyToMany(cascade = CascadeType.REMOVE)
private List<Course> courses; // ¡Eliminaría los cursos!
orphanRemoval
¿Qué es?
Eliminar automáticamente las entidades que ya no están referenciadas.
@OneToMany(mappedBy = "department", orphanRemoval = true)
private List<Employee> employees;
Comportamiento:
Department dept = deptRepo.findById(1L);
dept.getEmployees().remove(0); // Quita de la lista
deptRepo.save(dept);
// ✅ ¡El empleado se ELIMINA de la BD!
Sin orphanRemoval:
// ❌ El empleado permanece en la BD con department_id = NULL
Relaciones Bidireccionales
¿Qué es?
Ambas entidades se conocen mutuamente.
// Employee conoce su Department
@ManyToOne
private Department department;
// Department conoce sus Employees
@OneToMany(mappedBy = "department")
private List<Employee> employees;
Sincronizar Ambos Lados
Métodos helper recomendados:
@Entity
public class Department {
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
// ✅ Helper para sincronizar
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // ¡Sincroniza!
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null); // ¡Sincroniza!
}
}
Uso:
Department dept = new Department("IT");
Employee emp = new Employee("Alice");
// ✅ CORRECTO - Usa el helper
dept.addEmployee(emp);
// ❌ INCORRECTO - Olvida sincronizar
dept.getEmployees().add(emp); // ¡Olvida emp.setDepartment(dept)!
Resumen del Capítulo
Los 3 Tipos de Relaciones
| Tipo | Anotación | Ejemplo | ¿Tabla de Unión? |
|---|---|---|---|
| 1-1 | @OneToOne | User ↔ Profile | No |
| N-1 | @ManyToOne | Employee → Department | No |
| 1-N | @OneToMany | Department → Employees | No |
| N-N | @ManyToMany | Student ↔ Course | Sí |
Fetch Types
EAGER: Carga inmediatamente (JOIN)
LAZY: Carga bajo demanda (recomendado)
Cascade
PERSIST: Guardado en cascada
REMOVE: Eliminación en cascada
ALL: Todas las operaciones
Autoevaluación
- ¿Qué anotación para "un departamento tiene varios empleados"?
- ¿Cuál es la diferencia entre LAZY y EAGER?
- ¿Qué hace
orphanRemoval = true? - ¿Por qué sincronizar las relaciones bidireccionales?
Respuestas:
@OneToManyen Department,@ManyToOneen Employee- LAZY carga bajo demanda, EAGER carga inmediatamente
- Elimina las entidades retiradas de la colección
- Para que ambos lados de la relación permanezcan coherentes
Próximo Paso
¡Domina las relaciones! Pase a las Consultas Personalizadas con @Query.