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
- Os 3 tipos de relações (@OneToOne, @OneToMany/@ManyToOne, @ManyToMany)
- Fetch types (LAZY vs EAGER)
- Operações de cascade
- 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
| Tipo | Anotação | Exemplo | Tabela de Junção? |
|---|---|---|---|
| 1-1 | @OneToOne | User ↔ Profile | Não |
| N-1 | @ManyToOne | Employee → Department | Não |
| 1-N | @OneToMany | Department → Employees | Não |
| N-N | @ManyToMany | Student ↔ Course | Sim |
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
- Qual anotação para "um departamento tem vários funcionários"?
- Qual a diferença entre LAZY e EAGER?
- O que faz
orphanRemoval = true? - Por que sincronizar relações bidirecionais?
Respostas:
@OneToManyem Department,@ManyToOneem Employee- LAZY carrega sob demanda, EAGER carrega imediatamente
- Remove as entidades retiradas da coleção
- 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.