Pular para o conteúdo principal

Aula 10: JPQL, Consultas Avançadas e Performance

Objetivos

  • Dominar JPQL (Jakarta Persistence Query Language) e suas construções avançadas
  • Implementar consultas dinâmicas com Criteria API para cenários de busca flexível
  • Aplicar Named Queries para consultas reutilizáveis e validadas em tempo de deploy
  • Configurar paginação eficiente e ordenação para grandes volumes de dados
  • Otimizar performance com Entity Graphs, cache de segundo nível e batch fetching
  • Utilizar Panache como alternativa produtiva ao EntityManager tradicional

Contexto e Motivação

Nas aulas anteriores você aprendeu a mapear entidades (Aula 8) e seus relacionamentos (Aula 9). Agora é hora de extrair valor desses dados. Em qualquer aplicação real, a maioria das operações envolve consultas — buscar filmes por gênero, listar avaliações recentes, encontrar os itens mais bem avaliados, filtrar por múltiplos critérios combinados.

Imagine o CritiqueHub com milhares de usuários e dezenas de milhares de avaliações. Sem consultas bem projetadas, a aplicação se torna inutilizável: telas que demoram segundos para carregar, timeouts em buscas, consumo excessivo de memória. A diferença entre uma aplicação profissional e uma amadora está justamente na qualidade das consultas e na estratégia de acesso a dados.

O JPA oferece três mecanismos complementares de consulta, cada um com seu cenário ideal:

MecanismoQuando UsarVantagem Principal
JPQLConsultas conhecidas em tempo de desenvolvimentoLegibilidade e proximidade com SQL
Criteria APIConsultas dinâmicas com filtros opcionaisType-safety e composição programática
Native QuerySQL específico do banco ou otimizações avançadasAcesso completo ao SQL nativo

Conteúdo

JPQL — Jakarta Persistence Query Language

JPQL é uma linguagem de consulta orientada a objetos que opera sobre entidades e seus atributos, não sobre tabelas e colunas. Embora sua sintaxe lembre SQL, há diferenças fundamentais:

AspectoSQLJPQL
Opera sobreTabelas e colunasEntidades e atributos
Nomesitem_culturalItemCultural
JoinsExplícitos via FKNavegação por relacionamentos
HerançaNão nativoSuporte nativo (TREAT, TYPE)
ResultadoLinhas e colunasObjetos gerenciados

Sintaxe Fundamental

// SELECT básico — retorna entidades gerenciadas
TypedQuery<Filme> query = em.createQuery(
"SELECT f FROM Filme f WHERE f.anoLancamento = :ano",
Filme.class
);
query.setParameter("ano", 2024);
List<Filme> filmes = query.getResultList();

Observe que Filme é o nome da entidade (classe Java), não o nome da tabela. O atributo anoLancamento é o campo Java, não a coluna ano_lancamento. O JPA traduz automaticamente para o SQL correto.

Projeções e DTOs

Nem sempre queremos retornar entidades completas. Projeções permitem selecionar apenas os campos necessários, reduzindo tráfego de dados e consumo de memória:

// Projeção com DTO usando construtor
TypedQuery<FilmeResumoDTO> query = em.createQuery(
"""
SELECT new br.edu.ifsp.critiquehub.dto.FilmeResumoDTO(
f.id, f.titulo, f.anoLancamento, AVG(a.nota)
)
FROM Filme f LEFT JOIN f.avaliacoes a
GROUP BY f.id, f.titulo, f.anoLancamento
HAVING AVG(a.nota) >= :notaMinima
ORDER BY AVG(a.nota) DESC
""",
FilmeResumoDTO.class
);
query.setParameter("notaMinima", 4.0);
List<FilmeResumoDTO> ranking = query.getResultList();

O DTO correspondente deve ter um construtor público compatível:

public record FilmeResumoDTO(
Long id,
String titulo,
int anoLancamento,
Double mediaNotas
) {}
Records como DTOs

Java Records são ideais para DTOs em projeções JPQL: imutáveis, compactos e com construtor canônico gerado automaticamente. Use-os sempre que possível.

Joins em JPQL

JPQL permite navegar pelos relacionamentos mapeados nas entidades, eliminando a necessidade de cláusulas JOIN explícitas com condições de FK:

// JOIN implícito — navegação por path
// O JPA gera o JOIN automaticamente ao navegar f.generos
TypedQuery<Filme> query = em.createQuery(
"SELECT f FROM Filme f WHERE f.diretor.nome = :diretor",
Filme.class
);

// JOIN explícito — necessário quando precisamos referenciar a entidade joinada
TypedQuery<Filme> query = em.createQuery(
"""
SELECT DISTINCT f FROM Filme f
JOIN f.generos g
WHERE g.nome IN :generos
""",
Filme.class
);
query.setParameter("generos", List.of("Ficção Científica", "Thriller"));

// LEFT JOIN — inclui filmes sem avaliações
TypedQuery<Object[]> query = em.createQuery(
"""
SELECT f.titulo, COUNT(a), COALESCE(AVG(a.nota), 0)
FROM Filme f LEFT JOIN f.avaliacoes a
GROUP BY f.titulo
ORDER BY COUNT(a) DESC
""",
Object[].class
);

Fetch Join vs Join Regular

A diferença entre JOIN e JOIN FETCH é crucial para performance:

// JOIN regular — gera N+1 ao acessar as avaliações depois
List<Filme> filmes = em.createQuery(
"SELECT f FROM Filme f JOIN f.generos g WHERE g.nome = :genero",
Filme.class
).setParameter("genero", "Drama").getResultList();
// Ao iterar filmes e chamar f.getAvaliacoes(), cada chamada gera um SELECT adicional!

// JOIN FETCH — carrega tudo em uma única query
List<Filme> filmes = em.createQuery(
"""
SELECT DISTINCT f FROM Filme f
JOIN f.generos g
JOIN FETCH f.avaliacoes
WHERE g.nome = :genero
""",
Filme.class
).setParameter("genero", "Drama").getResultList();
// As avaliações já estão carregadas — nenhum SELECT adicional
Atenção com múltiplos FETCH JOINs

Nunca use JOIN FETCH em mais de uma coleção na mesma query. Isso gera um produto cartesiano que multiplica as linhas retornadas, causando o erro "cannot simultaneously fetch multiple bags" e problemas graves de performance.

Funções JPQL

JPQL oferece funções integradas que cobrem os cenários mais comuns:

CategoriaFunçõesExemplo
StringCONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTHLOWER(f.titulo) LIKE :termo
NuméricaABS, SQRT, MOD, SIZESIZE(f.avaliacoes) > 10
DataCURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMPa.dataCriacao > :desde
AgregaçãoCOUNT, SUM, AVG, MIN, MAXAVG(a.nota)
ColeçãoSIZE, IS EMPTY, MEMBER OFf.generos IS NOT EMPTY

Named Queries

Named Queries são consultas nomeadas, definidas na entidade e validadas em tempo de deploy. Se houver erro de sintaxe JPQL, a aplicação falha ao iniciar — não em runtime:

@Entity
@NamedQueries({
@NamedQuery(
name = "Filme.buscarPorGenero",
query = """
SELECT DISTINCT f FROM Filme f
JOIN f.generos g
WHERE g.nome = :genero
ORDER BY f.titulo
"""
),
@NamedQuery(
name = "Filme.rankingPorNota",
query = """
SELECT f FROM Filme f
LEFT JOIN f.avaliacoes a
GROUP BY f
HAVING COUNT(a) >= :minimoAvaliacoes
ORDER BY AVG(a.nota) DESC
"""
),
@NamedQuery(
name = "Filme.buscarPorTermo",
query = """
SELECT f FROM Filme f
WHERE LOWER(f.titulo) LIKE LOWER(CONCAT('%', :termo, '%'))
OR LOWER(f.sinopse) LIKE LOWER(CONCAT('%', :termo, '%'))
ORDER BY f.titulo
"""
)
})
public class Filme {
// ... campos e mapeamentos
}

Uso no repositório:

@ApplicationScoped
public class FilmeRepository {

@Inject
EntityManager em;

public List<Filme> buscarPorGenero(String genero) {
return em.createNamedQuery("Filme.buscarPorGenero", Filme.class)
.setParameter("genero", genero)
.getResultList();
}

public List<Filme> ranking(int minimoAvaliacoes, int limite) {
return em.createNamedQuery("Filme.rankingPorNota", Filme.class)
.setParameter("minimoAvaliacoes", minimoAvaliacoes)
.setMaxResults(limite)
.getResultList();
}
}
Convenção de nomenclatura

Use o padrão Entidade.operacao para nomes de Named Queries: Filme.buscarPorGenero, Usuario.ativosPorRegiao. Isso mantém as queries organizadas e facilmente localizáveis no código.

Criteria API — Consultas Dinâmicas

A Criteria API é a solução do JPA para consultas que precisam ser montadas dinamicamente com base em filtros opcionais. Enquanto JPQL é ideal para consultas fixas, a Criteria API brilha quando o usuário pode combinar diferentes critérios de busca.

Cenário Motivador

Imagine a tela de busca avançada do CritiqueHub, onde o usuário pode filtrar por título, gênero, ano, nota mínima — qualquer combinação. Com JPQL, você precisaria construir a string de query concatenando condições, o que é frágil e propenso a SQL injection. Com Criteria API, a construção é type-safe e programática:

@ApplicationScoped
public class FilmeRepository {

@Inject
EntityManager em;

public List<Filme> buscarComFiltros(FilmeFiltroDTO filtro) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Filme> cq = cb.createQuery(Filme.class);
Root<Filme> filme = cq.from(Filme.class);

// Lista de predicados — cada filtro preenchido adiciona uma condição
List<Predicate> predicados = new ArrayList<>();

if (filtro.titulo() != null && !filtro.titulo().isBlank()) {
predicados.add(
cb.like(cb.lower(filme.get("titulo")),
"%" + filtro.titulo().toLowerCase() + "%")
);
}

if (filtro.anoMinimo() != null) {
predicados.add(
cb.greaterThanOrEqualTo(filme.get("anoLancamento"), filtro.anoMinimo())
);
}

if (filtro.anoMaximo() != null) {
predicados.add(
cb.lessThanOrEqualTo(filme.get("anoLancamento"), filtro.anoMaximo())
);
}

if (filtro.generos() != null && !filtro.generos().isEmpty()) {
Join<Filme, Genero> generoJoin = filme.join("generos");
predicados.add(generoJoin.get("nome").in(filtro.generos()));
cq.distinct(true); // evita duplicatas do JOIN
}

// Combina todos os predicados com AND
cq.where(cb.and(predicados.toArray(new Predicate[0])));

// Ordenação dinâmica
if ("nota".equals(filtro.ordenarPor())) {
cq.orderBy(cb.desc(filme.get("mediaNotas")));
} else {
cq.orderBy(cb.asc(filme.get("titulo")));
}

return em.createQuery(cq)
.setFirstResult(filtro.pagina() * filtro.tamanhoPagina())
.setMaxResults(filtro.tamanhoPagina())
.getResultList();
}
}

O DTO de filtro:

public record FilmeFiltroDTO(
String titulo,
Integer anoMinimo,
Integer anoMaximo,
List<String> generos,
String ordenarPor,
int pagina,
int tamanhoPagina
) {
public FilmeFiltroDTO {
if (tamanhoPagina <= 0) tamanhoPagina = 20;
if (pagina < 0) pagina = 0;
}
}

Metamodel — Type-Safety Completa

Os exemplos acima usam strings para nomes de atributos ("titulo", "anoLancamento"). Se você renomear um campo, o erro só aparecerá em runtime. O JPA Metamodel elimina esse risco gerando classes de metadados em tempo de compilação:

// Classe gerada automaticamente: Filme_ (com underscore)
// Usar em vez de strings:
predicados.add(
cb.like(cb.lower(filme.get(Filme_.titulo)),
"%" + filtro.titulo().toLowerCase() + "%")
);

predicados.add(
cb.greaterThanOrEqualTo(filme.get(Filme_.anoLancamento), filtro.anoMinimo())
);

Para gerar o Metamodel no Quarkus, adicione ao pom.xml:

<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<scope>provided</scope>
</dependency>

Paginação e Ordenação

Aplicações reais nunca retornam todos os registros de uma vez. A paginação é essencial para performance e usabilidade.

Paginação com EntityManager

@ApplicationScoped
public class FilmeRepository {

@Inject
EntityManager em;

public PaginaDTO<Filme> listarPaginado(int pagina, int tamanho, String ordenarPor) {
// 1. Contar total de registros
Long total = em.createQuery("SELECT COUNT(f) FROM Filme f", Long.class)
.getSingleResult();

// 2. Buscar página atual
String jpql = "SELECT f FROM Filme f ORDER BY f." + ordenarPor;
List<Filme> filmes = em.createQuery(jpql, Filme.class)
.setFirstResult(pagina * tamanho) // offset
.setMaxResults(tamanho) // limit
.getResultList();

// 3. Retornar DTO de página
int totalPaginas = (int) Math.ceil((double) total / tamanho);
return new PaginaDTO<>(filmes, pagina, tamanho, total, totalPaginas);
}
}

O DTO de página encapsula os metadados de paginação:

public record PaginaDTO<T>(
List<T> conteudo,
int paginaAtual,
int tamanhoPagina,
long totalElementos,
int totalPaginas
) {
public boolean temProxima() {
return paginaAtual < totalPaginas - 1;
}

public boolean temAnterior() {
return paginaAtual > 0;
}
}

Endpoint REST Paginado

@Path("/filmes")
@Produces(MediaType.APPLICATION_JSON)
public class FilmeResource {

@Inject
FilmeRepository filmeRepository;

@GET
public Response listar(
@QueryParam("pagina") @DefaultValue("0") int pagina,
@QueryParam("tamanho") @DefaultValue("20") int tamanho,
@QueryParam("ordenarPor") @DefaultValue("titulo") String ordenarPor) {

PaginaDTO<Filme> resultado = filmeRepository.listarPaginado(pagina, tamanho, ordenarPor);
return Response.ok(resultado).build();
}
}

Otimização de Performance

Problema N+1 — Diagnóstico e Solução

O problema N+1 é o vilão mais comum de performance no JPA. Ocorre quando o JPA executa 1 query para buscar N entidades e depois N queries adicionais para carregar coleções LAZY de cada uma:

// Cenário N+1: 1 + N queries
SELECT * FROM filme; -- 1 query (retorna 100 filmes)
SELECT * FROM avaliacao WHERE filme_id = 1; -- query 2
SELECT * FROM avaliacao WHERE filme_id = 2; -- query 3
... -- 100 queries adicionais!
SELECT * FROM avaliacao WHERE filme_id = 100; -- query 101

Solução 1 — Fetch Join (vista na Aula 9, aprofundada aqui):

// Uma única query com JOIN FETCH
List<Filme> filmes = em.createQuery("""
SELECT DISTINCT f FROM Filme f
LEFT JOIN FETCH f.avaliacoes
ORDER BY f.titulo
""", Filme.class).getResultList();
// Resultado: 1 query apenas

Solução 2 — Entity Graph (declarativa):

@Entity
@NamedEntityGraph(
name = "Filme.comAvaliacoesEGeneros",
attributeNodes = {
@NamedAttributeNode("avaliacoes"),
@NamedAttributeNode("generos")
}
)
public class Filme { /* ... */ }

// Uso no repositório
public List<Filme> listarComDetalhes() {
EntityGraph<?> graph = em.getEntityGraph("Filme.comAvaliacoesEGeneros");
return em.createQuery("SELECT f FROM Filme f", Filme.class)
.setHint("jakarta.persistence.fetchgraph", graph)
.getResultList();
}

Solução 3 — Batch Fetching (configuração global):

# application.properties — carrega coleções LAZY em lotes
quarkus.hibernate-orm.jdbc.statement-batch-size=25
quarkus.hibernate-orm.properties."hibernate.default_batch_fetch_size"=16

Com batch fetching configurado, o Hibernate agrupa as coleções LAZY em lotes de 16, transformando 100 queries em ~7 queries.

Comparação das Estratégias

EstratégiaPrósContrasQuando Usar
Fetch JoinControle preciso, 1 queryProduto cartesiano com múltiplas coleçõesQuando sabe quais relações precisa
Entity GraphDeclarativo, reutilizávelMenos flexível que fetch joinPadrões recorrentes de fetch
Batch FetchingGlobal, sem mudança em queriesNão é 1 query (são N/batch)Melhoria geral baseline
DTO ProjectionMínimo de dados trafegadosNão retorna entidades gerenciadasLeitura pura / relatórios

Cache de Segundo Nível

O JPA suporta dois níveis de cache. O cache de primeiro nível (L1) é o Persistence Context — automático e por transação. O cache de segundo nível (L2) persiste entre transações e é compartilhado:

# application.properties
quarkus.hibernate-orm.cache."br.edu.ifsp.critiquehub.model.Genero".expiration.max-idle=3600
quarkus.hibernate-orm.cache."br.edu.ifsp.critiquehub.model.Genero".memory.object-count=100
@Entity
@Cacheable // habilita cache L2 para esta entidade
public class Genero {
@Id @GeneratedValue
private Long id;
private String nome;
// ... Gêneros mudam raramente — candidactos ideais para cache
}
Quando NÃO usar cache L2

Evite cache L2 para entidades que mudam frequentemente (como Avaliacao), dados sensíveis, ou entidades com muitas associações. O custo de invalidação pode superar o benefício do cache.

Panache — Produtividade Quarkus

O Quarkus oferece o Hibernate Panache, uma camada de abstração que simplifica drasticamente o código de persistência. Existem dois padrões:

@Entity
public class Filme extends PanacheEntity {
// campos públicos — Panache gera getters/setters
public String titulo;
public int anoLancamento;
public String sinopse;

// Métodos de consulta como métodos estáticos da entidade
public static List<Filme> buscarPorGenero(String genero) {
return find("SELECT DISTINCT f FROM Filme f JOIN f.generos g WHERE g.nome = ?1",
genero).list();
}

public static List<Filme> buscarPorTermo(String termo) {
return find("LOWER(titulo) LIKE LOWER(?1)", "%" + termo + "%").list();
}

public static PanacheQuery<Filme> listarOrdenado() {
return findAll(Sort.by("titulo"));
}
}
// Uso no serviço — extremamente conciso
@Path("/filmes")
public class FilmeResource {

@GET
public List<Filme> listar() {
return Filme.listAll(Sort.by("titulo"));
}

@GET @Path("/busca")
public List<Filme> buscar(@QueryParam("q") String termo) {
return Filme.buscarPorTermo(termo);
}

@POST @Transactional
public Response criar(Filme filme) {
filme.persist();
return Response.status(Status.CREATED).entity(filme).build();
}

@DELETE @Path("/{id}") @Transactional
public void deletar(@PathParam("id") Long id) {
Filme.deleteById(id);
}
}

Paginação com Panache

Panache simplifica paginação para poucas linhas:

@GET
public Response listarPaginado(
@QueryParam("pagina") @DefaultValue("0") int pagina,
@QueryParam("tamanho") @DefaultValue("20") int tamanho) {

PanacheQuery<Filme> query = Filme.findAll(Sort.by("titulo"));
query.page(Page.of(pagina, tamanho));

return Response.ok(Map.of(
"conteudo", query.list(),
"totalPaginas", query.pageCount(),
"totalElementos", query.count(),
"paginaAtual", pagina
)).build();
}

Monitoramento de Queries — Hibernate Statistics

Para diagnosticar problemas de performance, habilite as estatísticas do Hibernate:

# application.properties — apenas em dev/test
%dev.quarkus.hibernate-orm.statistics=true
%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.hibernate-orm.log.bind-parameters=true

No log você verá:

Hibernate: select f1_0.id, f1_0.titulo, ... from filme f1_0 order by f1_0.titulo
Hibernate: Session Metrics {
123456 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
2 JDBC statements prepared;
1 JDBC statements executed;
0 JDBC batches executed;
}

Laboratório 5 — Consultas Avançadas no CritiqueHub

Objetivo

Implementar um módulo completo de busca e listagem para o CritiqueHub: ranking de filmes, busca por múltiplos critérios, paginação e otimização de performance.

Passo 1 — Named Queries na Entidade Filme

Adicione Named Queries à entidade Filme:

@Entity
@Table(name = "filme")
@NamedQueries({
@NamedQuery(
name = "Filme.ranking",
query = """
SELECT new br.edu.ifsp.critiquehub.dto.FilmeRankingDTO(
f.id, f.titulo, f.anoLancamento, AVG(a.nota), COUNT(a)
)
FROM Filme f LEFT JOIN f.avaliacoes a
GROUP BY f.id, f.titulo, f.anoLancamento
HAVING COUNT(a) >= :minAvaliacoes
ORDER BY AVG(a.nota) DESC
"""
),
@NamedQuery(
name = "Filme.porGeneroComAvaliacoes",
query = """
SELECT DISTINCT f FROM Filme f
JOIN f.generos g
LEFT JOIN FETCH f.avaliacoes
WHERE g.nome = :genero
ORDER BY f.titulo
"""
)
})
public class Filme {
// ... mapeamentos existentes
}

Passo 2 — Repository com Criteria API

Crie o método de busca dinâmica com Criteria API:

@ApplicationScoped
public class FilmeRepository {

@Inject
EntityManager em;

public PaginaDTO<Filme> buscaAvancada(FilmeFiltroDTO filtro) {
CriteriaBuilder cb = em.getCriteriaBuilder();

// Query de contagem
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<Filme> countRoot = countQuery.from(Filme.class);
List<Predicate> countPredicados = buildPredicados(cb, countRoot, filtro);
countQuery.select(cb.countDistinct(countRoot));
if (!countPredicados.isEmpty()) {
countQuery.where(cb.and(countPredicados.toArray(new Predicate[0])));
}
Long total = em.createQuery(countQuery).getSingleResult();

// Query de dados
CriteriaQuery<Filme> dataQuery = cb.createQuery(Filme.class);
Root<Filme> dataRoot = dataQuery.from(Filme.class);
List<Predicate> dataPredicados = buildPredicados(cb, dataRoot, filtro);
dataQuery.select(dataRoot).distinct(true);
if (!dataPredicados.isEmpty()) {
dataQuery.where(cb.and(dataPredicados.toArray(new Predicate[0])));
}
dataQuery.orderBy(cb.asc(dataRoot.get("titulo")));

List<Filme> filmes = em.createQuery(dataQuery)
.setFirstResult(filtro.pagina() * filtro.tamanhoPagina())
.setMaxResults(filtro.tamanhoPagina())
.getResultList();

int totalPaginas = (int) Math.ceil((double) total / filtro.tamanhoPagina());
return new PaginaDTO<>(filmes, filtro.pagina(), filtro.tamanhoPagina(), total, totalPaginas);
}

private List<Predicate> buildPredicados(CriteriaBuilder cb, Root<Filme> root, FilmeFiltroDTO filtro) {
List<Predicate> predicados = new ArrayList<>();

if (filtro.titulo() != null && !filtro.titulo().isBlank()) {
predicados.add(cb.like(cb.lower(root.get("titulo")),
"%" + filtro.titulo().toLowerCase() + "%"));
}
if (filtro.anoMinimo() != null) {
predicados.add(cb.greaterThanOrEqualTo(root.get("anoLancamento"), filtro.anoMinimo()));
}
if (filtro.generos() != null && !filtro.generos().isEmpty()) {
Join<Filme, ?> generoJoin = root.join("generos");
predicados.add(generoJoin.get("nome").in(filtro.generos()));
}

return predicados;
}

// Ranking usando Named Query
public List<FilmeRankingDTO> ranking(int minAvaliacoes, int limite) {
return em.createNamedQuery("Filme.ranking", FilmeRankingDTO.class)
.setParameter("minAvaliacoes", (long) minAvaliacoes)
.setMaxResults(limite)
.getResultList();
}
}

Passo 3 — Endpoint REST com Busca e Paginação

@Path("/filmes")
@Produces(MediaType.APPLICATION_JSON)
public class FilmeResource {

@Inject
FilmeRepository filmeRepository;

@GET
@Path("/busca")
public Response buscaAvancada(
@QueryParam("titulo") String titulo,
@QueryParam("anoMinimo") Integer anoMinimo,
@QueryParam("anoMaximo") Integer anoMaximo,
@QueryParam("genero") List<String> generos,
@QueryParam("pagina") @DefaultValue("0") int pagina,
@QueryParam("tamanho") @DefaultValue("20") int tamanho) {

var filtro = new FilmeFiltroDTO(titulo, anoMinimo, anoMaximo, generos, "titulo", pagina, tamanho);
PaginaDTO<Filme> resultado = filmeRepository.buscaAvancada(filtro);

return Response.ok(resultado).build();
}

@GET
@Path("/ranking")
public Response ranking(
@QueryParam("minAvaliacoes") @DefaultValue("5") int minAvaliacoes,
@QueryParam("limite") @DefaultValue("10") int limite) {

List<FilmeRankingDTO> ranking = filmeRepository.ranking(minAvaliacoes, limite);
return Response.ok(ranking).build();
}
}

Passo 4 — Teste Integrado

@QuarkusTest
class FilmeResourceTest {

@Test
void deveRetornarRankingDeFilmes() {
given()
.queryParam("minAvaliacoes", 1)
.queryParam("limite", 5)
.when()
.get("/filmes/ranking")
.then()
.statusCode(200)
.body("$.size()", lessThanOrEqualTo(5));
}

@Test
void deveBuscarFilmesPorTitulo() {
given()
.queryParam("titulo", "matrix")
.queryParam("pagina", 0)
.queryParam("tamanho", 10)
.when()
.get("/filmes/busca")
.then()
.statusCode(200)
.body("totalElementos", greaterThanOrEqualTo(0))
.body("conteudo", notNullValue());
}

@Test
void devePaginarResultados() {
given()
.queryParam("pagina", 0)
.queryParam("tamanho", 5)
.when()
.get("/filmes/busca")
.then()
.statusCode(200)
.body("tamanhoPagina", is(5))
.body("paginaAtual", is(0));
}
}

Exercícios

  1. Crie uma Named Query Filme.lancamentosRecentes que retorne filmes lançados nos últimos N anos, ordenados por ano decrescente
  2. Implemente um método Criteria API que permita buscar avaliações por nota mínima, data e usuário — todos filtros opcionais
  3. Adicione paginação ao endpoint de ranking, retornando a página solicitada e os metadados de navegação
  4. Configure Entity Graph para carregar filmes com gêneros e avaliações em uma única query e compare o número de queries no log
  5. Refatore o FilmeRepository para usar Panache Repository, mantendo a mesma API pública
  6. Implemente um endpoint /filmes/estatisticas que retorne: total de filmes, média geral de notas, gênero mais popular e filme mais avaliado — usando projeções JPQL

Referências