Pular para o conteúdo principal

Aula 9: JPA — Mapeando Relacionamentos

Objetivos

  • Compreender cardinalidade (1:1, 1:N, N:1, N:N) e direcionalidade (uni/bidirecional)
  • Implementar relacionamentos @OneToMany, @ManyToOne e @ManyToMany com JPA
  • Utilizar @Embeddable, @EmbeddedId e @MapsId para chaves compostas e value objects
  • Resolver problemas de performance (N+1, Cartesian Product) com fetch joins e Entity Graphs
  • Aplicar boas práticas arquiteturais para manutenção de integridade bidirecional

Contexto e Motivação

Em sistemas de informação empresariais, os dados raramente existem de forma isolada. A eficácia de uma aplicação depende fundamentalmente de sua capacidade de modelar e gerenciar as associações entre entidades que compõem o domínio de negócio.

No CritiqueHub, nosso domínio apresenta uma estrutura de dados altamente interconectada:

Entidade OrigemRelacionamentoEntidade Destino
Usuário1:NAvaliação
AvaliaçãoN:1Item Cultural
Item CulturalN:NGênero
UsuárioN:NUsuário (Seguidores)
ListaN:1Usuário
FilmeN:1Diretor
FilmeN:NAtor

Sem o JPA, o gerenciamento dessas associações exigiria controle manual de integridade referencial, navegação por múltiplas queries SQL, sincronização de estado e otimização manual de consultas. O JPA abstrai essa complexidade — usuario.getAvaliacoes() é traduzido automaticamente para SQL otimizado.

Conteúdo

Cardinalidade e Direcionamento

Cardinalidade

TipoDescriçãoExemplo CritiqueHub
One-to-One (1:1)Cada instância associada a no máximo uma da outraUsuário ↔ PerfilDetalhado
One-to-Many (1:N)Uma instância proprietária → múltiplas dependentesUsuário → Avaliações
Many-to-One (N:1)Múltiplas dependentes → uma proprietáriaAvaliações → ItemCultural
Many-to-Many (N:N)Múltiplas em ambos os ladosItemCultural ↔ Gênero

Direcionamento

Unidirecional: Apenas uma entidade mantém referência à outra.

@Entity
public class Avaliacao {
@ManyToOne
@JoinColumn(name = "usuario_id")
private Usuario usuario; // Avaliacao → Usuario (sentido único)
}
// Usuario NÃO possui referência para suas avaliações

Bidirecional: Ambas mantêm referências mútuas. Requer gerenciamento de sincronização.

@Entity
public class Usuario {
@OneToMany(mappedBy = "usuario") // Lado inverso
private List<Avaliacao> avaliacoes = new ArrayList<>();
}

@Entity
public class Avaliacao {
@ManyToOne
@JoinColumn(name = "usuario_id") // Lado proprietário (dono da FK)
private Usuario usuario;
}

Relacionamento One-to-Many / Many-to-One

O padrão mais frequente em aplicações empresariais. No CritiqueHub: Usuário→Avaliações, ItemCultural→Avaliações, Usuário→Listas.

Padrão recomendado — bidirecional com métodos de sincronização:

@Entity
@Table(name = "usuarios")
public class Usuario extends BaseEntity {

@OneToMany(mappedBy = "usuario", cascade = {CascadeType.PERSIST, CascadeType.MERGE},
orphanRemoval = true, fetch = FetchType.LAZY)
private List<Avaliacao> avaliacoes = new ArrayList<>();

// Métodos de conveniência para manter consistência bidirecional
public void adicionarAvaliacao(Avaliacao avaliacao) {
avaliacoes.add(avaliacao);
avaliacao.setUsuario(this);
}

public void removerAvaliacao(Avaliacao avaliacao) {
avaliacoes.remove(avaliacao);
avaliacao.setUsuario(null);
}

// Retorna coleção imutável para proteger a integridade
public List<Avaliacao> getAvaliacoes() {
return Collections.unmodifiableList(avaliacoes);
}
}

@Entity
@Table(name = "avaliacoes")
public class Avaliacao extends BaseEntity {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "usuario_id", nullable = false)
private Usuario usuario;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_cultural_id", nullable = false)
private ItemCultural itemCultural;

@Column(nullable = false)
private Integer nota;

@Column(length = 2000)
private String conteudo;
}

Cascade Types

TipoComportamentoQuando Usar
PERSISTPersiste filho quando pai é persistidoCriação em cascata
MERGEAtualiza filho quando pai é atualizadoAtualização em cascata
REMOVERemove filho quando pai é removidoExclusão dependentes
ALLTodos os efeitos combinadosUse com cuidado

Fetch Strategies

EstratégiaComportamentoQuando Usar
LAZY (padrão para coleções)Carrega sob demandaMaioria dos casos
EAGER (padrão para @ManyToOne)Carrega imediatamenteDados sempre necessários
aviso

LAZY é quase sempre a melhor escolha para coleções. EAGER pode causar carregamento desnecessário de grandes volumes de dados e N+1 queries.

Classes @Embeddable e Value Objects

@Embeddable permite criar value objects e chaves compostas que são mapeados diretamente na tabela da entidade proprietária.

// Value Object — Endereço reutilizável
@Embeddable
public class Endereco {
@Column(length = 200)
private String rua;
@Column(length = 100)
private String cidade;
@Column(length = 2)
private String estado;
@Column(length = 10)
private String cep;

// equals() e hashCode() obrigatórios!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Endereco endereco = (Endereco) o;
return Objects.equals(rua, endereco.rua) && Objects.equals(cidade, endereco.cidade);
}

@Override
public int hashCode() { return Objects.hash(rua, cidade, estado, cep); }
}

Chave composta com @EmbeddedId e @MapsId:

// Chave composta para UsuarioSeguidor
@Embeddable
public class UsuarioSeguidorId implements Serializable {
@Column(name = "seguidor_id")
private Long seguidorId;
@Column(name = "seguido_id")
private Long seguidoId;

// equals() e hashCode() obrigatórios
}

// Entidade de junção com atributos extras
@Entity
@Table(name = "usuario_seguidores")
public class UsuarioSeguidor {

@EmbeddedId
private UsuarioSeguidorId id;

@MapsId("seguidorId") // Deriva FK do relacionamento
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seguidor_id")
private Usuario seguidor;

@MapsId("seguidoId")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seguido_id")
private Usuario seguido;

@Column(name = "data_inicio", nullable = false)
private LocalDateTime dataInicio;

@PrePersist
private void validarAutoSeguimento() {
if (id.getSeguidorId().equals(id.getSeguidoId())) {
throw new IllegalStateException("Usuário não pode seguir a si mesmo");
}
}
}

Relacionamento Many-to-Many

@Entity
@Table(name = "generos")
public class Genero extends BaseEntity {

@Column(nullable = false, unique = true, length = 50)
private String nome;

@ManyToMany(mappedBy = "generos")
private List<ItemCultural> itensCulturais = new ArrayList<>();
}

@Entity
@Table(name = "itens_culturais")
@Inheritance(strategy = InheritanceType.JOINED)
public class ItemCultural extends BaseEntity {

@ManyToMany
@JoinTable(
name = "item_cultural_genero",
joinColumns = @JoinColumn(name = "item_cultural_id"),
inverseJoinColumns = @JoinColumn(name = "genero_id")
)
private List<Genero> generos = new ArrayList<>();

// Métodos de sincronização
public void adicionarGenero(Genero genero) {
if (!generos.contains(genero)) {
generos.add(genero);
genero.getItensCulturais().add(this);
}
}
}

Estratégias de Otimização de Performance

O Problema N+1

Quando o JPA executa 1 query para buscar N entidades e depois N queries adicionais para carregar coleções LAZY de cada uma.

Solução — Fetch Join:

@ApplicationScoped
public class UsuarioRepository {

@Inject EntityManager em;

// Fetch join resolve N+1 carregando tudo em 1 query
public List<Usuario> listarComAvaliacoes() {
return em.createQuery("""
SELECT DISTINCT u FROM Usuario u
LEFT JOIN FETCH u.avaliacoes a
WHERE u.status = :status
ORDER BY u.nomeCompleto
""", Usuario.class)
.setParameter("status", StatusUsuario.ATIVO)
.getResultList();
}

// Para paginação com fetch join: estratégia de 2 queries
public List<Usuario> listarComAvaliacoesPaginado(int pagina, int tam) {
// Query 1: busca IDs paginados
List<Long> ids = em.createQuery(
"SELECT u.id FROM Usuario u WHERE u.status = :status ORDER BY u.nomeCompleto", Long.class)
.setParameter("status", StatusUsuario.ATIVO)
.setFirstResult(pagina * tam).setMaxResults(tam)
.getResultList();

if (ids.isEmpty()) return List.of();

// Query 2: fetch join apenas para os IDs relevantes
return em.createQuery("""
SELECT DISTINCT u FROM Usuario u
LEFT JOIN FETCH u.avaliacoes
WHERE u.id IN :ids ORDER BY u.nomeCompleto
""", Usuario.class)
.setParameter("ids", ids)
.getResultList();
}
}

Entity Graphs

@Entity
@NamedEntityGraphs({
@NamedEntityGraph(name = "Usuario.comAvaliacoes",
attributeNodes = @NamedAttributeNode("avaliacoes")),
@NamedEntityGraph(name = "Usuario.completo",
attributeNodes = {
@NamedAttributeNode("avaliacoes"),
@NamedAttributeNode(value = "avaliacoes",
subgraph = "avaliacao-item")
},
subgraphs = @NamedSubgraph(name = "avaliacao-item",
attributeNodes = @NamedAttributeNode("itemCultural")))
})
public class Usuario extends BaseEntity { /* ... */ }

// Uso no repositório
public Optional<Usuario> buscarComAvaliacoes(Long id) {
EntityGraph<?> graph = em.getEntityGraph("Usuario.comAvaliacoes");
return Optional.ofNullable(
em.find(Usuario.class, id, Map.of("jakarta.persistence.fetchgraph", graph))
);
}

Boas Práticas Arquiteturais

  1. Métodos de conveniência para manter consistência bidirecional (adicionarAvaliacao/removerAvaliacao)
  2. Coleções defensivas com Collections.unmodifiableList() nos getters
  3. Validação via lifecycle callbacks (@PrePersist, @PreUpdate) para regras de integridade
  4. Builder pattern para construção de entidades complexas com múltiplos relacionamentos
  5. Hierarquia de exceções customizadas para erros de relacionamento

Laboratório 5 — Implementação Prática de Relacionamentos JPA

Exercício 1: Sistema de Tags (Many-to-Many)

Objetivo: Implementar TagItemCultural com @ManyToMany e @JoinTable.

@Entity
@Table(name = "tags")
public class Tag extends BaseEntity {

@Column(nullable = false, unique = true, length = 50)
private String nome;

@Column(length = 7)
private String cor; // Hex color: #FF5733

@ManyToMany(mappedBy = "tags")
private List<ItemCultural> itensCulturais = new ArrayList<>();

@PrePersist
private void gerarCorAleatoria() {
if (this.cor == null) {
this.cor = String.format("#%06X", new java.util.Random().nextInt(0xFFFFFF));
}
}
}

Implemente TagService com métodos buscarOuCriarTag(String nome) e buscarItensPorTag(String nomeTag).

Exercício 2: Lista de Favoritos (Many-to-Many com atributos)

Objetivo: Implementar ListaFavoritos como entidade de junção com @EmbeddedId e @MapsId.

@Embeddable
public class ListaFavoritosId implements Serializable {
@Column(name = "usuario_id")
private Long usuarioId;
@Column(name = "item_cultural_id")
private Long itemCulturalId;
// equals() e hashCode()
}

public enum CategoriaFavorito {
QUERO_ASSISTIR, RECOMENDADO, FAVORITO_ABSOLUTO, PARA_REVISITAR, ASSISTINDO
}

@Entity
@Table(name = "lista_favoritos")
public class ListaFavoritos {

@EmbeddedId
private ListaFavoritosId id;

@MapsId("usuarioId")
@ManyToOne(fetch = FetchType.LAZY)
private Usuario usuario;

@MapsId("itemCulturalId")
@ManyToOne(fetch = FetchType.LAZY)
private ItemCultural itemCultural;

@Enumerated(EnumType.STRING)
private CategoriaFavorito categoria;

@Column(length = 500)
private String observacoes;
}

Exercício 3: Sistema de Comentários (Self-referencing)

Objetivo: Implementar Comentario com auto-referência @ManyToOne/@OneToMany (máximo 1 nível de aninhamento).

@Entity
@Table(name = "comentarios")
public class Comentario extends BaseEntity {

@Column(nullable = false, length = 1000)
private String conteudo;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "avaliacao_id", nullable = false)
private Avaliacao avaliacao;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "autor_id", nullable = false)
private Usuario autor;

// Auto-referência: comentário pai (resposta)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "comentario_pai_id")
private Comentario comentarioPai;

@OneToMany(mappedBy = "comentarioPai", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comentario> respostas = new ArrayList<>();

@PrePersist
private void validarAninhamento() {
if (comentarioPai != null && comentarioPai.getComentarioPai() != null) {
throw new IllegalStateException("Comentários suportam apenas 1 nível de aninhamento");
}
}
}

Exercícios (Checkpoints)

  1. Implemente e teste o sistema de Tags (Exercício 1) com @QuarkusTest
  2. Crie o FavoritosService com métodos para adicionar, remover e listar favoritos por categoria
  3. Implemente o ComentarioService com verificação de autorização para remoção
  4. Resolva um cenário N+1 no repositório utilizando fetch join e Entity Graph
  5. Crie uma consulta Criteria API dinâmica que filtre usuários com base em múltiplos critérios opcionais

Referências