Saltar al contenido principal

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​

  1. Los 3 tipos de relaciones (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
  2. Tipos de fetch (LAZY vs EAGER)
  3. Operaciones en cascada
  4. 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​

TipoAnotaciónEjemplo¿Tabla de Unión?
1-1@OneToOneUser ↔ ProfileNo
N-1@ManyToOneEmployee → DepartmentNo
1-N@OneToManyDepartment → EmployeesNo
N-N@ManyToManyStudent ↔ CourseSí

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​

  1. ¿Qué anotación para "un departamento tiene varios empleados"?
  2. ¿Cuál es la diferencia entre LAZY y EAGER?
  3. ¿Qué hace orphanRemoval = true?
  4. ¿Por qué sincronizar las relaciones bidireccionales?

Respuestas:

  1. @OneToMany en Department, @ManyToOne en Employee
  2. LAZY carga bajo demanda, EAGER carga inmediatamente
  3. Elimina las entidades retiradas de la colección
  4. Para que ambos lados de la relación permanezcan coherentes

Próximo Paso​

¡Domina las relaciones! Pase a las Consultas Personalizadas con @Query.

➡️ Capítulo 3: @Query

⬅️ Capítulo anterior: Convenciones

⬅️ Volver al Módulo 6