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:
| Mecanismo | Quando Usar | Vantagem Principal |
|---|---|---|
| JPQL | Consultas conhecidas em tempo de desenvolvimento | Legibilidade e proximidade com SQL |
| Criteria API | Consultas dinâmicas com filtros opcionais | Type-safety e composição programática |
| Native Query | SQL específico do banco ou otimizações avançadas | Acesso 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:
| Aspecto | SQL | JPQL |
|---|---|---|
| Opera sobre | Tabelas e colunas | Entidades e atributos |
| Nomes | item_cultural | ItemCultural |
| Joins | Explícitos via FK | Navegação por relacionamentos |
| Herança | Não nativo | Suporte nativo (TREAT, TYPE) |
| Resultado | Linhas e colunas | Objetos 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
) {}
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
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:
| Categoria | Funções | Exemplo |
|---|---|---|
| String | CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH | LOWER(f.titulo) LIKE :termo |
| Numérica | ABS, SQRT, MOD, SIZE | SIZE(f.avaliacoes) > 10 |
| Data | CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP | a.dataCriacao > :desde |
| Agregação | COUNT, SUM, AVG, MIN, MAX | AVG(a.nota) |
| Coleção | SIZE, IS EMPTY, MEMBER OF | f.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();
}
}
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égia | Prós | Contras | Quando Usar |
|---|---|---|---|
| Fetch Join | Controle preciso, 1 query | Produto cartesiano com múltiplas coleções | Quando sabe quais relações precisa |
| Entity Graph | Declarativo, reutilizável | Menos flexível que fetch join | Padrões recorrentes de fetch |
| Batch Fetching | Global, sem mudança em queries | Não é 1 query (são N/batch) | Melhoria geral baseline |
| DTO Projection | Mínimo de dados trafegados | Não retorna entidades gerenciadas | Leitura 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
}
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:
- Active Record
- Repository Pattern
@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);
}
}
@Entity
public class Filme extends PanacheEntity {
public String titulo;
public int anoLancamento;
public String sinopse;
}
@ApplicationScoped
public class FilmeRepository implements PanacheRepository<Filme> {
public List<Filme> buscarPorGenero(String genero) {
return find("SELECT DISTINCT f FROM Filme f JOIN f.generos g WHERE g.nome = ?1",
genero).list();
}
public List<Filme> buscarPorTermo(String termo) {
return find("LOWER(titulo) LIKE LOWER(?1)", "%" + termo + "%").list();
}
public PanacheQuery<Filme> listarOrdenado() {
return findAll(Sort.by("titulo"));
}
}
@Path("/filmes")
public class FilmeResource {
@Inject
FilmeRepository filmeRepository;
@GET
public List<Filme> listar() {
return filmeRepository.listAll(Sort.by("titulo"));
}
@POST @Transactional
public Response criar(Filme filme) {
filmeRepository.persist(filme);
return Response.status(Status.CREATED).entity(filme).build();
}
}
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
- Crie uma Named Query
Filme.lancamentosRecentesque retorne filmes lançados nos últimos N anos, ordenados por ano decrescente - Implemente um método Criteria API que permita buscar avaliações por nota mínima, data e usuário — todos filtros opcionais
- Adicione paginação ao endpoint de ranking, retornando a página solicitada e os metadados de navegação
- 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
- Refatore o
FilmeRepositorypara usar Panache Repository, mantendo a mesma API pública - Implemente um endpoint
/filmes/estatisticasque retorne: total de filmes, média geral de notas, gênero mais popular e filme mais avaliado — usando projeções JPQL