Aula 8: Introdução ao JPA (Jakarta Persistence API)
Objetivos
- Compreender a evolução da persistência em Java e por que JPA é fundamental
- Dominar o ciclo de vida das entidades (NEW, MANAGED, DETACHED, REMOVED)
- Implementar mapeamento objeto-relacional com anotações JPA
- Construir entidades, repositórios e serviços de negócio para o CritiqueHub
- Configurar ambientes multi-perfil (dev, test, prod) com Quarkus 3.31
Contexto e Motivação
Esta aula explora a Jakarta Persistence API (JPA) como a solução padrão para persistência de dados em aplicações Jakarta EE. Você aprenderá a projetar e implementar camadas de persistência robustas, aplicando os conceitos diretamente no desenvolvimento do CritiqueHub.
Imagine um bibliotecário que precisa gerenciar milhares de livros sem nenhum sistema de catalogação. Cada vez que um livro é emprestado, devolvido ou cadastrado, ele precisa fazer isso manualmente. Essa é essencialmente a situação que os desenvolvedores Java enfrentavam antes do JPA: uma complexidade desnecessária para operações que deveriam ser simples.
O JPA é como um sistema de biblioteca automatizado — ele cuida de toda a "burocracia" de gerenciamento de dados para que você possa focar no que realmente importa: a lógica de negócio.
Conteúdo
O Contexto Histórico: De JDBC ao JPA
Era JDBC (1997–2005): Acesso direto ao banco de dados através de SQL, gerenciamento manual de conexões/statements/resultsets, mapeamento manual entre tabelas e objetos, código verboso e propenso a erros.
Era Hibernate/ORM (2005–2010): Introdução do conceito de ORM (Object-Relational Mapping), frameworks proprietários, melhoria significativa na produtividade, mas falta de padronização.
Era JPA (2010–presente): Padronização do ORM, API unificada independente do provider, integração nativa com containers enterprise, evolução contínua.
A Problemática da Persistência Tradicional
public class AvaliacaoDAO {
private DataSource dataSource;
public void salvarAvaliacao(Avaliacao avaliacao) throws SQLException {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
// 1. Obter conexão manualmente
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 2. Verificar dependências manualmente
stmt = conn.prepareStatement("SELECT id FROM usuarios WHERE id = ?");
stmt.setLong(1, avaliacao.getUsuarioId());
rs = stmt.executeQuery();
if (!rs.next()) {
throw new SQLException("Usuário não encontrado");
}
rs.close(); stmt.close();
// 3. SQL hardcoded e mapeamento manual
stmt = conn.prepareStatement(
"INSERT INTO avaliacoes (usuario_id, item_id, nota, resenha, " +
"data_criacao, status_consumo, recomenda) VALUES (?, ?, ?, ?, ?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
);
// 4. Mapeamento manual tedioso e propenso a erros
stmt.setLong(1, avaliacao.getUsuarioId());
stmt.setLong(2, avaliacao.getItemId());
stmt.setInt(3, avaliacao.getNota());
stmt.setString(4, avaliacao.getResenha());
stmt.setTimestamp(5, Timestamp.valueOf(avaliacao.getDataCriacao()));
stmt.setString(6, avaliacao.getStatusConsumo().name());
stmt.setBoolean(7, avaliacao.getRecomenda());
stmt.executeUpdate();
conn.commit();
} catch (SQLException e) {
if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} }
throw e;
} finally {
// Limpeza manual de recursos
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (stmt != null) try { stmt.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
}
}
| Problema | Impacto |
|---|---|
| Código Verboso | Operações simples requerem dezenas de linhas |
| Gerenciamento Manual de Recursos | Pontos de falha e vazamentos de memória |
| SQL Hardcoded | Dificulta manutenção e portabilidade |
| Mapeamento Manual | Tedioso, repetitivo e propenso a erros |
| Acoplamento com SGBD | Limita flexibilidade arquitetural |
Mapeamento Objeto-Relacional (ORM)
O que é ORM? Object-Relational Mapping é uma técnica que cria uma "ponte" entre o mundo orientado a objetos do Java e o mundo relacional dos bancos de dados SQL.
A mesma operação com JPA:
@ApplicationScoped
public class AvaliacaoService {
@Inject
EntityManager entityManager; // Interface central do JPA, injetada via CDI
@Transactional // Gerencia transação automaticamente
public void salvarAvaliacao(Avaliacao avaliacao) {
if (avaliacao.getUsuario() == null) {
throw new IllegalArgumentException("Usuário é obrigatório");
}
entityManager.persist(avaliacao); // Única linha necessária!
}
public Optional<Avaliacao> buscarPorId(Long id) {
return Optional.ofNullable(entityManager.find(Avaliacao.class, id));
}
}
Compare: 80+ linhas de JDBC vs 5 linhas de JPA para a mesma funcionalidade!
| Aspecto | Benefício | Impacto |
|---|---|---|
| Produtividade | Eliminação de 70-80% do código de infraestrutura | Desenvolvimento 3-5x mais rápido |
| Manutenibilidade | Centralização das regras de mapeamento | Redução de 60% no tempo de manutenção |
| Portabilidade | Abstração do banco de dados específico | Flexibilidade arquitetural |
| Testabilidade | Testes com mocks e bancos em memória | Cobertura de testes aumenta 40% |
| Performance | Lazy loading, cache de primeiro nível, batch processing | Melhorias de 20-50% |
Arquitetura e Componentes Essenciais
- EntityManager: Interface central — ponto de entrada para todas as operações de persistência
- Persistence Context: O "espaço" lógico onde entidades são gerenciadas (cache de primeiro nível)
- EntityManagerFactory: Factory para criar instâncias de EntityManager
- Persistence Unit: Configuração que define como entidades são persistidas
- JPA Provider: Implementação concreta (Hibernate no Quarkus)
Ciclo de Vida das Entidades
Entender os estados das entidades é crucial porque:
- Performance: Mudanças só são rastreadas em entidades MANAGED
- Transações: Apenas entidades MANAGED participam de transações
- Cache: O JPA só mantém cache para entidades MANAGED
- Relacionamentos: Navegação automática funciona apenas entre entidades gerenciadas
| Estado | Descrição | Analogia |
|---|---|---|
| NEW/TRANSIENT | Objeto criado via construtor, sem representação no banco | Carta escrita mas não enviada |
| MANAGED | Sob controle do EntityManager, mudanças detectadas automaticamente | Google Docs — salva automaticamente |
| DETACHED | Já persistida mas não mais gerenciada; mudanças NÃO são rastreadas | Cópia impressa de um documento |
| REMOVED | Marcada para remoção; será excluída no flush/commit | Arquivo na lixeira |
@Transactional
public void demonstrarCicloVida() {
// ESTADO NEW/TRANSIENT
Usuario usuario = new Usuario("joao@exemplo.com", "joao123", "João Silva");
// em.contains(usuario) == false
// TRANSIÇÃO PARA MANAGED
em.persist(usuario);
// em.contains(usuario) == true
// Dirty checking — mudanças detectadas automaticamente
usuario.setBiografia("Desenvolvedor Java");
// Não é necessário chamar update()
// TRANSIÇÃO PARA DETACHED
em.detach(usuario);
usuario.setBiografia("Nova bio"); // NÃO será persistida
// RETORNO PARA MANAGED
usuario = em.merge(usuario); // merge() retorna nova referência
// TRANSIÇÃO PARA REMOVED
em.remove(usuario);
}
Operações CRUD Fundamentais
@ApplicationScoped
public class CRUDExemplos {
@Inject EntityManager em;
// CREATE
@Transactional
public Usuario criar(Usuario usuario) {
em.persist(usuario); // Persiste e gera ID
return usuario;
}
// READ
public Optional<Usuario> buscarPorId(Long id) {
return Optional.ofNullable(em.find(Usuario.class, id));
}
// UPDATE
@Transactional
public Usuario atualizar(Usuario usuario) {
return em.merge(usuario); // Sempre use a referência retornada
}
// DELETE
@Transactional
public boolean deletar(Long id) {
Usuario usuario = em.find(Usuario.class, id);
if (usuario != null) {
em.remove(usuario);
return true;
}
return false;
}
}
Cuidados Importantes com CRUD:
persist()só funciona com entidades NEW/TRANSIENTmerge()sempre retorna uma nova referência — use-a!remove()só funciona com entidades MANAGEDrefresh()descarta mudanças não commitadasfind()utiliza cache automático — segunda chamada é quase instantânea
Mapeamento de Entidades
Anotações Fundamentais
@Entity // Marca a classe como entidade JPA
@Table(name = "usuarios") // Nome da tabela
public class Usuario {
@Id // Chave primária
@GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-incremento
private Long id;
@Column(name = "email", nullable = false, unique = true, length = 255)
private String email;
@Enumerated(EnumType.STRING) // Armazena nome do enum como string
@Column(name = "status", nullable = false, length = 20)
private StatusUsuario status;
}
| Estratégia | Descrição | Quando Usar |
|---|---|---|
| IDENTITY | Auto-incremento do banco | MySQL, PostgreSQL (pequenas aplicações) |
| SEQUENCE | Sequências do banco | PostgreSQL, Oracle (alta performance) |
| TABLE | Tabela dedicada para IDs | Portabilidade máxima |
| UUID | Identificadores únicos globais | Sistemas distribuídos |
Relacionamentos entre Entidades
// One-to-One
@Entity
public class Usuario {
@OneToOne(mappedBy = "usuario", cascade = CascadeType.ALL)
private Perfil perfil;
}
// One-to-Many / Many-to-One
@Entity
public class Usuario {
@OneToMany(mappedBy = "usuario", cascade = CascadeType.ALL)
private List<Avaliacao> avaliacoes;
}
@Entity
public class Avaliacao {
@ManyToOne
@JoinColumn(name = "usuario_id")
private Usuario usuario;
}
// Many-to-Many
@Entity
public class Grupo {
@ManyToMany
@JoinTable(name = "usuario_grupo",
joinColumns = @JoinColumn(name = "grupo_id"),
inverseJoinColumns = @JoinColumn(name = "usuario_id"))
private List<Usuario> usuarios;
}
Consultas com JPQL e Criteria API
// JPQL — linguagem de consultas orientada a objetos
public List<Usuario> buscarUsuariosAtivos() {
return em.createQuery(
"SELECT u FROM Usuario u WHERE u.status = :status", Usuario.class)
.setParameter("status", StatusUsuario.ATIVO)
.getResultList();
}
// Criteria API — consultas tipo seguro e dinâmicas
public List<Usuario> buscarPorNome(String nome) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Usuario> cq = cb.createQuery(Usuario.class);
Root<Usuario> usuario = cq.from(Usuario.class);
cq.select(usuario).where(cb.like(usuario.get("nomeCompleto"), "%" + nome + "%"));
return em.createQuery(cq).getResultList();
}
Laboratório 4 — JPA Básico com Quarkus
Objetivo
- Projetar entidades JPA robustas com validações, relacionamentos e auditoria automática
- Implementar repositórios especializados seguindo o padrão Repository
- Criar serviços de negócio com regras de domínio e transações
- Configurar ambientes multi-perfil (dev, test, prod) com Quarkus 3.31
Pré-requisitos
- JDK 21+, Quarkus CLI ou Maven configurado
- Extensões: Hibernate ORM with Panache, JDBC PostgreSQL, JDBC H2, RESTEasy Jackson, Hibernate Validator
Crie o projeto em code.quarkus.io:
- GroupId:
br.upf.ads175| ArtifactId:lab04-critiquehub-jpa| Version:1.0.0-SNAPSHOT
Passo 1: Estrutura Arquitetural
src/main/java/br/upf/ads175/critiquehub/
├── entity/
│ ├── model/ # Entidades JPA
│ └── enums/ # Enumerações
├── repository/ # Camada de acesso a dados
├── service/ # Serviços de negócio
├── resource/ # Endpoints REST
├── dto/ # Data Transfer Objects
├── exception/ # Exceções personalizadas
└── util/ # Utilitários
Passo 2: Configuração Multi-Ambiente
# Informações da aplicação
quarkus.application.name=CritiqueHub
quarkus.application.version=1.0.0-SNAPSHOT
# Produção — PostgreSQL
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=critiquehub_user
quarkus.datasource.password=${DB_PASSWORD:prodpassword}
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:critiquehub}
quarkus.datasource.jdbc.max-size=20
quarkus.hibernate-orm.schema-management.strategy=validate
quarkus.hibernate-orm.log.sql=false
# Desenvolvimento — H2 em memória
%dev.quarkus.datasource.db-kind=h2
%dev.quarkus.datasource.username=critiquehub
%dev.quarkus.datasource.password=dev123
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:critiquehub;DB_CLOSE_DELAY=-1
%dev.quarkus.hibernate-orm.schema-management.strategy=drop-and-create
%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.hibernate-orm.log.format-sql=true
# Testes — H2 isolado
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.username=test
%test.quarkus.datasource.password=test
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test-critiquehub;DB_CLOSE_DELAY=-1
%test.quarkus.hibernate-orm.schema-management.strategy=drop-and-create
Quarkus Dev Services: Quando você adiciona uma extensão de banco de dados, o Quarkus pode provisionar automaticamente um container Docker com o banco configurado, facilitando o desenvolvimento sem instalação manual.
Passo 3: Enumerações
// StatusUsuario.java
package br.upf.ads175.critiquehub.entity.enums;
public enum StatusUsuario {
ATIVO("Usuário ativo no sistema"),
SUSPENSO("Usuário temporariamente suspenso"),
INATIVO("Usuário inativo por escolha própria");
private final String descricao;
StatusUsuario(String descricao) { this.descricao = descricao; }
public String getDescricao() { return descricao; }
public boolean isAtivo() { return this == ATIVO; }
}
// TipoItem.java
public enum TipoItem {
FILME("Filme cinematográfico"),
LIVRO("Livro ou publicação literária"),
JOGO("Jogo eletrônico");
// ... construtor e métodos similares
}
// StatusConsumo.java
public enum StatusConsumo {
FINALIZADO("Completamente consumido"),
EM_ANDAMENTO("Atualmente consumindo"),
PLANEJADO("Planejado para consumo futuro");
// ... construtor e métodos similares
public boolean permiteAvaliacao() { return this == FINALIZADO || this == EM_ANDAMENTO; }
}
Passo 4: Entidade Base com Auditoria
package br.upf.ads175.critiquehub.entity.model;
import jakarta.persistence.*;
import java.util.Objects;
@MappedSuperclass // Não é entidade JPA, mas propriedades são herdadas
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@Version // Controle de concorrência otimista
protected Long versao;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getVersao() { return versao; }
public boolean isNew() { return this.id == null; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BaseEntity that = (BaseEntity) o;
if (this.id == null || that.id == null) return false;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return id != null ? Objects.hash(id) : super.hashCode();
}
@Override
public String toString() {
return String.format("%s{id=%d, versao=%d}", getClass().getSimpleName(), id, versao);
}
}
Passo 5: Entidade Usuario
package br.upf.ads175.critiquehub.entity.model;
import br.upf.ads175.critiquehub.entity.enums.StatusUsuario;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.Period;
@Entity
@Table(name = "usuarios")
@NamedQueries({
@NamedQuery(name = "Usuario.buscarPorEmail",
query = "SELECT u FROM Usuario u WHERE u.email = :email"),
@NamedQuery(name = "Usuario.buscarPorNomeUsuario",
query = "SELECT u FROM Usuario u WHERE u.nomeUsuario = :nomeUsuario"),
@NamedQuery(name = "Usuario.listarAtivos",
query = "SELECT u FROM Usuario u WHERE u.status = 'ATIVO' ORDER BY u.dataRegistro DESC")
})
public class Usuario extends BaseEntity {
@Column(name = "email", nullable = false, unique = true, length = 255)
private String email;
@Column(name = "nome_usuario", nullable = false, unique = true, length = 30)
private String nomeUsuario;
@Column(name = "nome_completo", nullable = false, length = 150)
private String nomeCompleto;
@Column(name = "biografia", length = 500)
private String biografia;
@Column(name = "data_nascimento")
private LocalDate dataNascimento;
@Column(name = "data_registro", nullable = false, updatable = false)
private LocalDate dataRegistro;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private StatusUsuario status;
@Column(name = "perfil_publico", nullable = false)
private Boolean perfilPublico = true;
// Construtor JPA (obrigatório)
protected Usuario() {
this.status = StatusUsuario.ATIVO;
this.dataRegistro = LocalDate.now();
}
// Construtor para criação de novos usuários
public Usuario(String email, String nomeUsuario, String nomeCompleto) {
this();
this.email = email;
this.nomeUsuario = nomeUsuario;
this.nomeCompleto = nomeCompleto;
}
// Métodos de Negócio
public boolean isAtivo() { return StatusUsuario.ATIVO.equals(this.status); }
public boolean isPerfilPublico() { return Boolean.TRUE.equals(perfilPublico); }
public Integer getIdade() {
if (dataNascimento == null) return null;
return Period.between(dataNascimento, LocalDate.now()).getYears();
}
// Getters e Setters
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getNomeUsuario() { return nomeUsuario; }
public void setNomeUsuario(String nomeUsuario) { this.nomeUsuario = nomeUsuario; }
public String getNomeCompleto() { return nomeCompleto; }
public void setNomeCompleto(String nomeCompleto) { this.nomeCompleto = nomeCompleto; }
public String getBiografia() { return biografia; }
public void setBiografia(String biografia) { this.biografia = biografia; }
public LocalDate getDataNascimento() { return dataNascimento; }
public void setDataNascimento(LocalDate dataNascimento) { this.dataNascimento = dataNascimento; }
public LocalDate getDataRegistro() { return dataRegistro; }
public StatusUsuario getStatus() { return status; }
public void setStatus(StatusUsuario status) { this.status = status; }
public Boolean getPerfilPublico() { return perfilPublico; }
public void setPerfilPublico(Boolean perfilPublico) { this.perfilPublico = perfilPublico; }
@Override
public String toString() {
return String.format("Usuario{id=%d, nomeUsuario='%s', status=%s}", id, nomeUsuario, status);
}
}
Passo 6: Camada de Repositório
package br.upf.ads175.critiquehub.repository;
import br.upf.ads175.critiquehub.entity.enums.StatusUsuario;
import br.upf.ads175.critiquehub.entity.model.Usuario;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class UsuarioRepository {
@Inject
EntityManager entityManager;
public Usuario salvar(Usuario usuario) {
entityManager.persist(usuario);
return usuario;
}
public Usuario atualizar(Usuario usuario) {
return entityManager.merge(usuario);
}
public Optional<Usuario> buscarPorId(Long id) {
return Optional.ofNullable(entityManager.find(Usuario.class, id));
}
public boolean remover(Long id) {
return buscarPorId(id).map(usuario -> {
entityManager.remove(usuario);
return true;
}).orElse(false);
}
public Optional<Usuario> buscarPorEmail(String email) {
try {
Usuario usuario = entityManager
.createNamedQuery("Usuario.buscarPorEmail", Usuario.class)
.setParameter("email", email.toLowerCase().trim())
.getSingleResult();
return Optional.of(usuario);
} catch (NoResultException e) {
return Optional.empty();
}
}
public boolean existePorEmail(String email) {
Long count = entityManager.createQuery(
"SELECT COUNT(u) FROM Usuario u WHERE LOWER(u.email) = LOWER(:email)", Long.class)
.setParameter("email", email.trim())
.getSingleResult();
return count > 0;
}
public boolean existePorNomeUsuario(String nomeUsuario) {
Long count = entityManager.createQuery(
"SELECT COUNT(u) FROM Usuario u WHERE u.nomeUsuario = :nomeUsuario", Long.class)
.setParameter("nomeUsuario", nomeUsuario.trim())
.getSingleResult();
return count > 0;
}
public List<Usuario> listarAtivos() {
return entityManager.createNamedQuery("Usuario.listarAtivos", Usuario.class).getResultList();
}
public List<Usuario> listarAtivos(int pagina, int tamanhoPagina) {
return entityManager.createNamedQuery("Usuario.listarAtivos", Usuario.class)
.setFirstResult(pagina * tamanhoPagina)
.setMaxResults(tamanhoPagina)
.getResultList();
}
public List<Usuario> buscarPorNome(String fragmentoNome, boolean apenasAtivos, int limite) {
String jpql = """
SELECT u FROM Usuario u
WHERE LOWER(u.nomeCompleto) LIKE LOWER(:fragmento)
OR LOWER(u.nomeUsuario) LIKE LOWER(:fragmento)
""";
if (apenasAtivos) jpql += " AND u.status = :status";
jpql += " ORDER BY u.nomeCompleto";
var query = entityManager.createQuery(jpql, Usuario.class)
.setParameter("fragmento", "%" + fragmentoNome.trim() + "%")
.setMaxResults(limite);
if (apenasAtivos) query.setParameter("status", StatusUsuario.ATIVO);
return query.getResultList();
}
}
Passo 7: Camada de Serviço
package br.upf.ads175.critiquehub.service;
import br.upf.ads175.critiquehub.entity.enums.StatusUsuario;
import br.upf.ads175.critiquehub.entity.model.Usuario;
import br.upf.ads175.critiquehub.exception.*;
import br.upf.ads175.critiquehub.repository.UsuarioRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class UsuarioService {
@Inject
UsuarioRepository usuarioRepository;
@Transactional
public Usuario criarUsuario(String email, String nomeUsuario, String nomeCompleto) {
validarDadosObrigatorios(email, nomeUsuario, nomeCompleto);
verificarDuplicacao(email, nomeUsuario);
Usuario novoUsuario = new Usuario(email.trim().toLowerCase(), nomeUsuario.trim(), nomeCompleto.trim());
return usuarioRepository.salvar(novoUsuario);
}
@Transactional
public Usuario atualizarPerfil(Long id, String nomeCompleto, String biografia) {
Usuario usuario = buscarPorIdObrigatorio(id);
if (nomeCompleto != null && !nomeCompleto.trim().isEmpty()) {
usuario.setNomeCompleto(nomeCompleto.trim());
}
usuario.setBiografia(biografia != null ? biografia.trim() : null);
return usuarioRepository.atualizar(usuario);
}
@Transactional
public Usuario alterarStatus(Long id, StatusUsuario novoStatus) {
Usuario usuario = buscarPorIdObrigatorio(id);
if (usuario.getStatus() == StatusUsuario.SUSPENSO && novoStatus == StatusUsuario.INATIVO) {
throw new RegraDeNegocioException("Usuário suspenso não pode ser marcado como inativo diretamente");
}
usuario.setStatus(novoStatus);
return usuarioRepository.atualizar(usuario);
}
public Usuario buscarPorIdObrigatorio(Long id) {
return usuarioRepository.buscarPorId(id)
.orElseThrow(() -> new EntidadeNaoEncontradaException("Usuário não encontrado com ID: " + id));
}
public List<Usuario> listarUsuariosAtivos() {
return usuarioRepository.listarAtivos();
}
@Transactional
public boolean removerDefinitivamente(Long id) {
return usuarioRepository.remover(id);
}
private void validarDadosObrigatorios(String email, String nomeUsuario, String nomeCompleto) {
if (email == null || email.trim().isEmpty()) throw new IllegalArgumentException("Email é obrigatório");
if (nomeUsuario == null || nomeUsuario.trim().isEmpty()) throw new IllegalArgumentException("Nome de usuário é obrigatório");
if (nomeCompleto == null || nomeCompleto.trim().isEmpty()) throw new IllegalArgumentException("Nome completo é obrigatório");
if (!email.contains("@")) throw new IllegalArgumentException("Email deve ter formato válido");
if (nomeUsuario.trim().length() < 3) throw new IllegalArgumentException("Nome de usuário deve ter pelo menos 3 caracteres");
}
private void verificarDuplicacao(String email, String nomeUsuario) {
if (usuarioRepository.existePorEmail(email.trim().toLowerCase()))
throw new DadosDuplicadosException("Email já está em uso: " + email);
if (usuarioRepository.existePorNomeUsuario(nomeUsuario.trim()))
throw new DadosDuplicadosException("Nome de usuário já está em uso: " + nomeUsuario);
}
}
Passo 8: Testes de Integração
package br.upf.ads175.critiquehub.service;
import br.upf.ads175.critiquehub.entity.model.Usuario;
import br.upf.ads175.critiquehub.exception.*;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
@DisplayName("Testes de Integração para UsuarioService")
class UsuarioServiceIntegrationTest {
@Inject
UsuarioService usuarioService;
@Test
@Transactional // Rollback automático ao final do teste
@DisplayName("Deve executar o ciclo CRUD completo para um usuário")
void deveExecutarCicloCRUDCompleto() {
// CREATE
Usuario criado = usuarioService.criarUsuario("crud@email.com", "crudteste", "CRUD Teste");
assertNotNull(criado.getId());
// READ
Usuario lido = usuarioService.buscarPorIdObrigatorio(criado.getId());
assertEquals("crudteste", lido.getNomeUsuario());
// UPDATE
Usuario atualizado = usuarioService.atualizarPerfil(lido.getId(), lido.getNomeCompleto(), "Nova bio");
assertEquals("Nova bio", atualizado.getBiografia());
// DELETE
Long idDeletado = atualizado.getId();
usuarioService.removerDefinitivamente(idDeletado);
// VERIFY DELETE
assertThrows(EntidadeNaoEncontradaException.class,
() -> usuarioService.buscarPorIdObrigatorio(idDeletado));
}
@Test
@Transactional
@DisplayName("Não deve permitir email duplicado")
void naoDeveCriarUsuarioComEmailDuplicado() {
usuarioService.criarUsuario("duplicado@email.com", "user1", "Usuário Um");
DadosDuplicadosException ex = assertThrows(DadosDuplicadosException.class,
() -> usuarioService.criarUsuario("duplicado@email.com", "user2", "Usuário Dois"));
assertTrue(ex.getMessage().contains("Email já está em uso"));
}
@Test
@Transactional
@DisplayName("Deve lançar exceção para ID inexistente")
void deveLancarExcecaoParaIdInexistente() {
assertThrows(EntidadeNaoEncontradaException.class,
() -> usuarioService.buscarPorIdObrigatorio(9999L));
}
}
Exercícios (Checkpoints)
- Implemente testes unitários para
UsuarioServicemockando oUsuarioRepository - Crie as entidades
Critica,FilmeeGeneroseguindo os mesmos padrões da entidadeUsuario - Implemente repositórios e serviços correspondentes para cada nova entidade
- Adicione paginação ao endpoint de listagem de usuários ativos
- Crie uma exceção customizada para validação de idade mínima e integre-a ao serviço