Capítulo 06: Relacionamentos JPA (@OneToMany, @ManyToOne) e FetchType
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Relacionamentos JPA (@OneToMany, @ManyToOne) e FetchType"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Relacionamentos JPA (@OneToMany, @ManyToOne) e FetchType
O lado “dono” da relação e mappedBy. Em uma associação bidirecional, apenas um dos lados possui a coluna de chave estrangeira — esse é o lado “dono” (owning side), identificado por @JoinColumn. O lado inverso usa mappedBy apontando para o campo do dono, informando ao Hibernate “não crie outra coluna, apenas espelhe essa relação em memória”. Em ItemPedido↔Pedido, ItemPedido é o dono (possui pedido_id); Pedido.itens é declarado com @OneToMany(mappedBy = "pedido").
FetchType.EAGER vs FetchType.LAZY. Por padrão, @ManyToOne/@OneToOne são EAGER e @OneToMany/@ManyToMany são LAZY. Em produção, a prática recomendada é declarar LAZY explicitamente em toda associação: carregar coleções inteiras a cada SELECT da entidade-pai — mesmo sem uso — desperdiça memória e I/O, e piora ainda mais o problema N+1. Com LAZY, o Hibernate substitui a coleção/entidade por um proxy que só dispara a consulta real ao ser acessado pela primeira vez.
LazyInitializationException. Se esse acesso ocorrer fora do escopo do EntityManager/transação que carregou a entidade (por exemplo, ao serializar a entidade para JSON em um Controller depois que o Service já retornou), o Hibernate não tem mais uma sessão ativa para buscar os dados e lança LazyInitializationException. As soluções corretas são: (1) acessar a associação ainda dentro do método @Transactional; (2) usar JOIN FETCH em JPQL para trazer tudo em uma única consulta; ou (3) usar @EntityGraph no repositório.
O problema N+1. Buscar uma lista de N pedidos e depois acessar pedido.getItens() de cada um, um a um, gera 1 consulta inicial + N consultas adicionais (uma por pedido) — o clássico problema N+1, devastador em listas grandes. JOIN FETCH ou @EntityGraph resolvem isso reduzindo para uma única consulta com JOIN.
cascade e orphanRemoval. CascadeType.ALL propaga operações (persist, merge, remove…) da entidade-pai para os filhos: salvar um Pedido novo salva automaticamente seus ItemPedido sem precisar chamar save() em cada item. orphanRemoval = true vai além: se um item for removido da coleção Java (pedido.getItens().remove(item)), o Hibernate emite um DELETE para aquele item no próximo flush, mesmo sem cascade = REMOVE explícito — essencial para manter a integridade em relações “todo-parte” (agregados DDD).
@ManyToMany e a tabela de junção. Relacionamentos N:N exigem uma tabela intermediária, declarada com @JoinTable(name, joinColumns, inverseJoinColumns) no lado dono. Em produção, quando a tabela de junção precisa de colunas extras (data de vínculo, papel, etc.), o @ManyToMany direto se torna insuficiente — modela-se então uma entidade explícita para a associação, com dois @ManyToOne.
💻 2. Código de Demonstração Corporativo
package com.empresa.api.model;
import jakarta.persistence.*;
@Entity
public class ItemPedido {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // lado "dono": possui a FK pedido_id
@JoinColumn(name = "pedido_id", nullable = false)
private Pedido pedido;
private Integer quantidade;
protected ItemPedido() {}
public ItemPedido(Pedido pedido, Integer quantidade) {
this.pedido = pedido;
this.quantidade = quantidade;
}
}
package com.empresa.api.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Pedido {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Lado inverso: mappedBy referencia o campo "pedido" em ItemPedido.
// cascade + orphanRemoval garantem que itens removidos da lista sejam apagados no banco.
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ItemPedido> itens = new ArrayList<>();
public void adicionarItem(ItemPedido item) {
itens.add(item);
item.setPedido(this); // sincroniza os dois lados da relação bidirecional
}
}
package com.empresa.api.repository;
import com.empresa.api.model.Pedido;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PedidoRepository extends JpaRepository<Pedido, Long> {
// Elimina o N+1: um único SELECT com LEFT JOIN carregando "itens" junto.
@EntityGraph(attributePaths = {"itens"})
List<Pedido> findAll();
// Alternativa explícita via JPQL com JOIN FETCH.
@Query("SELECT DISTINCT p FROM Pedido p LEFT JOIN FETCH p.itens WHERE p.id = :id")
Pedido buscarComItens(Long id);
}