Pular para o conteúdo principal

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) {}
}
}
}
ProblemaImpacto
Código VerbosoOperações simples requerem dezenas de linhas
Gerenciamento Manual de RecursosPontos de falha e vazamentos de memória
SQL HardcodedDificulta manutenção e portabilidade
Mapeamento ManualTedioso, repetitivo e propenso a erros
Acoplamento com SGBDLimita flexibilidade arquitetural

Mapeamento Objeto-Relacional (ORM)

dica

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!

AspectoBenefícioImpacto
ProdutividadeEliminação de 70-80% do código de infraestruturaDesenvolvimento 3-5x mais rápido
ManutenibilidadeCentralização das regras de mapeamentoRedução de 60% no tempo de manutenção
PortabilidadeAbstração do banco de dados específicoFlexibilidade arquitetural
TestabilidadeTestes com mocks e bancos em memóriaCobertura de testes aumenta 40%
PerformanceLazy loading, cache de primeiro nível, batch processingMelhorias 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

important

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
EstadoDescriçãoAnalogia
NEW/TRANSIENTObjeto criado via construtor, sem representação no bancoCarta escrita mas não enviada
MANAGEDSob controle do EntityManager, mudanças detectadas automaticamenteGoogle Docs — salva automaticamente
DETACHEDJá persistida mas não mais gerenciada; mudanças NÃO são rastreadasCópia impressa de um documento
REMOVEDMarcada para remoção; será excluída no flush/commitArquivo 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;
}
}
cuidado

Cuidados Importantes com CRUD:

  1. persist() só funciona com entidades NEW/TRANSIENT
  2. merge() sempre retorna uma nova referência — use-a!
  3. remove() só funciona com entidades MANAGED
  4. refresh() descarta mudanças não commitadas
  5. find() 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égiaDescriçãoQuando Usar
IDENTITYAuto-incremento do bancoMySQL, PostgreSQL (pequenas aplicações)
SEQUENCESequências do bancoPostgreSQL, Oracle (alta performance)
TABLETabela dedicada para IDsPortabilidade máxima
UUIDIdentificadores únicos globaisSistemas 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

src/main/resources/application.properties
# 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
dica

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)

  1. Implemente testes unitários para UsuarioService mockando o UsuarioRepository
  2. Crie as entidades Critica, Filme e Genero seguindo os mesmos padrões da entidade Usuario
  3. Implemente repositórios e serviços correspondentes para cada nova entidade
  4. Adicione paginação ao endpoint de listagem de usuários ativos
  5. Crie uma exceção customizada para validação de idade mínima e integre-a ao serviço

Referências