Pular para o conteúdo principal

Capítulo 2: Relações entre Entidades

Introdução​

No mundo real, os dados estão relacionados:

  • Um Cliente faz vários Pedidos
  • Um Departamento contém vários Funcionários
  • Um Estudante cursa várias Disciplinas

Este capítulo ensina como modelar essas relações com JPA!

O que você vai aprender​

  1. Os 3 tipos de relações (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
  2. Fetch types (LAZY vs EAGER)
  3. Operações de cascade
  4. Relações bidirecionais

Duração: 1h30


Problema Sem Relações​

Abordagem Ingênua (Ruim)​

@Entity
public class Employee {
@Id
private Long id;
private String name;
private Long departmentId; // ❌ Apenas um ID!
}

@Entity
public class Department {
@Id
private Long id;
private String name;
}

Problemas:

// Para recuperar o departamento de um funcionário:
Employee emp = empRepo.findById(1L);
Department dept = deptRepo.findById(emp.getDepartmentId()); // 2 consultas!

// Para recuperar todos os funcionários de um departamento:
List<Employee> emps = empRepo.findByDepartmentId(deptId); // Sem relação!

Solução: Relações JPA​

Com Relações (Bom)​

@Entity
public class Employee {
@Id
private Long id;
private String name;

@ManyToOne // ✅ Relação JPA!
private Department department;
}

@Entity
public class Department {
@Id
private Long id;
private String name;

@OneToMany(mappedBy = "department") // ✅ Relação inversa!
private List<Employee> employees;
}

Vantagens:

// Departamento de um funcionário - FÁCIL!
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment(); // Acesso direto!

// Funcionários de um departamento - FÁCIL!
Department dept = deptRepo.findById(1L);
List<Employee> emps = dept.getEmployees(); // Acesso direto!

Tipo 1: @OneToOne (1-1)​

O que é?​

Uma entidade ligada a UMA ÚNICA outra entidade.

Exemplos:

  • User ↔ UserProfile (um usuário tem um perfil)
  • Person ↔ Passport (uma pessoa tem um passaporte)
  • Employee ↔ ParkingSpot (um funcionário tem uma vaga de estacionamento)

Código: User e UserProfile​

Entidade User:

@Entity
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;
private String email;

// Relação OneToOne
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id") // Chave estrangeira na tabela users
private UserProfile profile;

// Construtores, getters, setters
}

Entidade UserProfile:

@Entity
public class UserProfile {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String bio;
private String avatar;

// Relação inversa (opcional)
@OneToOne(mappedBy = "profile")
private User user;

// Construtores, getters, setters
}

Utilização​

// Criar
User user = new User("john", "john@email.com");
UserProfile profile = new UserProfile("Bio de John", "avatar.jpg");
user.setProfile(profile);
userRepo.save(user); // Profile salvo automaticamente (cascade)

// Ler
User user = userRepo.findById(1L);
UserProfile profile = user.getProfile();
System.out.println(profile.getBio());

Tipo 2: @ManyToOne e @OneToMany (N-1 / 1-N)​

O que é?​

Várias entidades ligadas a UMA entidade.

Exemplos:

  • Department ↔ Employees (um departamento tem vários funcionários)
  • Author ↔ Books (um autor escreve vários livros)
  • Category ↔ Products (uma categoria contém vários produtos)

Código: Department e Employee​

Lado Many (Employee):

@Entity
public class Employee {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
private Double salary;

// MUITOS funcionários pertencem a UM departamento
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id") // FK na tabela employee
private Department department;

// Construtores
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;

// UM departamento tem MUITOS funcionários
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees = new ArrayList<>();

// Construtores
public Department() {}

public Department(String name) {
this.name = name;
}

// Métodos helper
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // Ligação bidirecional
}

public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}

// Getters, setters
}

Utilização​

// Criar um departamento com funcionários
Department it = new Department("IT");
it.addEmployee(new Employee("Alice", 50000.0));
it.addEmployee(new Employee("Bob", 60000.0));
deptRepo.save(it); // Funcionários salvos automaticamente

// Ler os funcionários de um departamento
Department dept = deptRepo.findById(1L);
List<Employee> employees = dept.getEmployees();
employees.forEach(emp -> System.out.println(emp.getName()));

// Ler o departamento de um funcionário
Employee emp = empRepo.findById(1L);
Department dept = emp.getDepartment();
System.out.println(dept.getName());

Tipo 3: @ManyToMany (N-N)​

O que é?​

Várias entidades ligadas a VÁRIAS entidades.

Exemplos:

  • Students ↔ Courses (um estudante faz vários cursos, um curso tem vários estudantes)
  • Books ↔ Authors (um livro tem vários autores, um autor escreve vários livros)
  • Products ↔ Tags (um produto tem várias tags, uma tag em vários produtos)

Necessita de uma tabela de junção!


Código: Student e Course​

Entidade Student:

@Entity
public class Student {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;

// MUITOS estudantes cursam MUITAS disciplinas
@ManyToMany
@JoinTable(
name = "student_course", // Tabela de junção
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);
}

// Construtores, getters, setters
}

Entidade Course:

@Entity
public class Course {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String title;

// Relação inversa
@ManyToMany(mappedBy = "courses")
private List<Student> students = new ArrayList<>();

// Construtores, getters, setters
}

Tabela gerada:

CREATE TABLE student_course (
student_id BIGINT,
course_id BIGINT,
PRIMARY KEY (student_id, course_id)
);

Utilização​

// Criar
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);

// Ler os cursos de um estudante
Student student = studentRepo.findById(1L);
List<Course> courses = student.getCourses();

// Ler os estudantes de um curso
Course course = courseRepo.findById(1L);
List<Student> students = course.getStudents();

Fetch Types: LAZY vs EAGER​

O Problema​

Quando carregar as entidades relacionadas?

Employee emp = empRepo.findById(1L);
// Pergunta: O Department já está carregado agora?

EAGER (Carregamento Imediato)​

@ManyToOne(fetch = FetchType.EAGER)
private Department department;

Comportamento:

Employee emp = empRepo.findById(1L);
// Department já está carregado!
Department dept = emp.getDepartment(); // Sem consulta adicional

SQL gerado:

SELECT e.*, d.* 
FROM employee e
LEFT JOIN department d ON e.department_id = d.id
WHERE e.id = 1;

Vantagens:

  • ✅ Dados disponíveis imediatamente
  • ✅ Sem consulta adicional

Desvantagens:

  • ❌ Carrega dados mesmo se não usados
  • ❌ Pode ser lento se houver muitas relações

LAZY (Carregamento Sob Demanda) - Recomendado​

@ManyToOne(fetch = FetchType.LAZY)
private Department department;

Comportamento:

Employee emp = empRepo.findById(1L);
// Department AINDA NÃO carregado (proxy)
Department dept = emp.getDepartment(); // Consulta AGORA

SQL gerado:

-- 1ª consulta
SELECT * FROM employee WHERE id = 1;

-- 2ª consulta (somente se acessar department)
SELECT * FROM department WHERE id = ?;

Vantagens:

  • ✅ Carrega apenas se necessário
  • ✅ Melhor performance geral

Desvantagens:

  • ⚠️ Consulta adicional se acessar
  • ⚠️ Pode causar o problema N+1

Quando Usar Qual?​

// ✅ LAZY para @OneToMany (lista)
@OneToMany(fetch = FetchType.LAZY) // Padrão
private List<Employee> employees;

// ✅ LAZY para @ManyToMany (lista)
@ManyToMany(fetch = FetchType.LAZY) // Padrão
private List<Course> courses;

// ⚠️ LAZY para @ManyToOne (recomendado)
@ManyToOne(fetch = FetchType.LAZY)
private Department department;

// ⚠️ EAGER apenas se SEMPRE utilizado
@ManyToOne(fetch = FetchType.EAGER)
private Department department; // Apenas se sempre necessário

Regra de ouro: Use LAZY por padrão!


Cascade Operations​

O Problema​

O que acontece quando salvo/excluo uma entidade com relações?

Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));

deptRepo.save(dept);
// Pergunta: O funcionário também é salvo?

Tipos de Cascade​

public enum CascadeType {
PERSIST, // Salvar em cascata
MERGE, // Atualizar em cascata
REMOVE, // Remover em cascata
REFRESH, // Atualizar em cascata
DETACH, // Desanexar em cascata
ALL // Todas as operações
}

Cascade.PERSIST (Salvar)​

@OneToMany(mappedBy = "department", cascade = CascadeType.PERSIST)
private List<Employee> employees;

Comportamento:

Department dept = new Department("IT");
dept.addEmployee(new Employee("Alice"));
deptRepo.save(dept);
// ✅ Employee salvo automaticamente!

Cascade.REMOVE (Remover)​

@OneToMany(mappedBy = "department", cascade = CascadeType.REMOVE)
private List<Employee> employees;

Comportamento:

deptRepo.deleteById(1L);  
// ⚠️ TODOS os funcionários do departamento são removidos!

Atenção: Use com cuidado!


Cascade.ALL (Tudo)​

@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
private List<Employee> employees;

Comportamento:

// Salvar, atualizar, remover em cascata
deptRepo.save(dept); // Funcionários salvos
deptRepo.delete(dept); // Funcionários removidos

Recomendações​

// ✅ BOM - Parent owns children
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items; // OrderItems pertencem ao Order

// ⚠️ ATENÇÃO - Entidades compartilhadas
@ManyToOne(cascade = CascadeType.PERSIST)
private Department department; // NÃO use REMOVE!

// ❌ PERIGOSO
@ManyToMany(cascade = CascadeType.REMOVE)
private List<Course> courses; // Removeria os cursos!

orphanRemoval​

O que é?​

Remove automaticamente entidades que não são mais referenciadas.

@OneToMany(mappedBy = "department", orphanRemoval = true)
private List<Employee> employees;

Comportamento:

Department dept = deptRepo.findById(1L);
dept.getEmployees().remove(0); // Remove da lista
deptRepo.save(dept);
// ✅ O funcionário é REMOVIDO do BD!

Sem orphanRemoval:

// ❌ O funcionário permanece no BD com department_id = NULL

Relações Bidirecionais​

O que é?​

Ambas as entidades conhecem uma à outra.

// Employee conhece seu Department
@ManyToOne
private Department department;

// Department conhece seus Employees
@OneToMany(mappedBy = "department")
private List<Employee> employees;

Sincronizar os Dois 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!
}
}

Utilização:

Department dept = new Department("IT");
Employee emp = new Employee("Alice");

// ✅ BOM - Usa o helper
dept.addEmployee(emp);

// ❌ RUIM - Esquece de sincronizar
dept.getEmployees().add(emp); // Esqueceu de emp.setDepartment(dept)!

Resumo do Capítulo​

Os 3 Tipos de Relações​

TipoAnotaçãoExemploTabela de Junção?
1-1@OneToOneUser ↔ ProfileNão
N-1@ManyToOneEmployee → DepartmentNão
1-N@OneToManyDepartment → EmployeesNão
N-N@ManyToManyStudent ↔ CourseSim

Fetch Types​

EAGER: Carrega imediatamente (JOIN)
LAZY: Carrega sob demanda (recomendado)

Cascade​

PERSIST: Salvar em cascata
REMOVE: Remover em cascata
ALL: Todas as operações

Autoavaliação​

  1. Qual anotação para "um departamento tem vários funcionários"?
  2. Qual a diferença entre LAZY e EAGER?
  3. O que faz orphanRemoval = true?
  4. Por que sincronizar relações bidirecionais?

Respostas:

  1. @OneToMany em Department, @ManyToOne em Employee
  2. LAZY carrega sob demanda, EAGER carrega imediatamente
  3. Remove as entidades retiradas da coleção
  4. Para que ambos os lados da relação permaneçam consistentes

Próximo Passo​

Você domina as relações! Avance para Consultas Personalizadas com @Query.

➡️ Capítulo 3: @Query

⬅️ Capítulo anterior: Convenções

⬅️ Voltar ao Módulo 6