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);
}