Aula 9: JPA — Mapeando Relacionamentos
Objetivos
- Compreender cardinalidade (1:1, 1:N, N:1, N:N) e direcionalidade (uni/bidirecional)
- Implementar relacionamentos
@OneToMany,@ManyToOnee@ManyToManycom JPA - Utilizar
@Embeddable,@EmbeddedIde@MapsIdpara 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 Origem | Relacionamento | Entidade Destino |
|---|---|---|
| Usuário | 1:N | Avaliação |
| Avaliação | N:1 | Item Cultural |
| Item Cultural | N:N | Gênero |
| Usuário | N:N | Usuário (Seguidores) |
| Lista | N:1 | Usuário |
| Filme | N:1 | Diretor |
| Filme | N:N | Ator |
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
| Tipo | Descrição | Exemplo CritiqueHub |
|---|---|---|
| One-to-One (1:1) | Cada instância associada a no máximo uma da outra | Usuário ↔ PerfilDetalhado |
| One-to-Many (1:N) | Uma instância proprietária → múltiplas dependentes | Usuário → Avaliações |
| Many-to-One (N:1) | Múltiplas dependentes → uma proprietária | Avaliações → ItemCultural |
| Many-to-Many (N:N) | Múltiplas em ambos os lados | ItemCultural ↔ 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
| Tipo | Comportamento | Quando Usar |
|---|---|---|
| PERSIST | Persiste filho quando pai é persistido | Criação em cascata |
| MERGE | Atualiza filho quando pai é atualizado | Atualização em cascata |
| REMOVE | Remove filho quando pai é removido | Exclusão dependentes |
| ALL | Todos os efeitos combinados | Use com cuidado |
Fetch Strategies
| Estratégia | Comportamento | Quando Usar |
|---|---|---|
| LAZY (padrão para coleções) | Carrega sob demanda | Maioria dos casos |
EAGER (padrão para @ManyToOne) | Carrega imediatamente | Dados sempre necessários |
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
- Métodos de conveniência para manter consistência bidirecional (
adicionarAvaliacao/removerAvaliacao) - Coleções defensivas com
Collections.unmodifiableList()nos getters - Validação via lifecycle callbacks (
@PrePersist,@PreUpdate) para regras de integridade - Builder pattern para construção de entidades complexas com múltiplos relacionamentos
- 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 Tag ↔ ItemCultural 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)
- Implemente e teste o sistema de Tags (Exercício 1) com
@QuarkusTest - Crie o
FavoritosServicecom métodos para adicionar, remover e listar favoritos por categoria - Implemente o
ComentarioServicecom verificação de autorização para remoção - Resolva um cenário N+1 no repositório utilizando fetch join e Entity Graph
- Crie uma consulta Criteria API dinâmica que filtre usuários com base em múltiplos critérios opcionais