Este guia detalha abordagens sofisticadas para construção de consultas utilizando Spring Data JPA, abrangendo desde métodos de repositório derivados e o uso de anotações @Query (com JPQL e SQL nativo) até a flexibilidade das Specifications. Serão abordados cenários comuns como filtragem por texto, manipulação de datas, ordenação, paginação e construção de consultas dinâmicas com múltiplos critérios.
- Configuração da Entidade
Para ilustrar os diversos padrões de consulta, utilizaremos a entidade MensagemChat como exemplo:
@Data
@Entity
@Table(name = "mensagem_chat")
public class MensagemChat {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer idUsuario;
@Column(columnDefinition = "text")
private String conteudo;
@Column(name = "data_criacao")
private LocalDateTime dataCriacao;
}
- Consultas Derivadas por Nomenclatura de Métodos
O Spring Data JPA oferece a funcionalidade de criar consultas automaticamente baseadas na assinatura dos métodos do seu repositório, eliminando a necessidade de implementar o SQL manualmente. A seguir, uma tabela com os principais operadores e exemplos:
| Palavra-Chave | Exemplo de Método | Fragmento SQL Gerado |
|---|---|---|
And |
findByidUsuarioAndConteudo |
WHERE id_usuario = ? AND conteudo = ? |
Or |
findByidUsuarioOrConteudo |
WHERE id_usuario = ? OR conteudo = ? |
Between |
findByDataCriacaoBetween |
WHERE data_criacao BETWEEN ? AND ? |
LessThan |
findByDataCriacaoLessThan |
WHERE data_criacao < ? |
GreaterThan |
findByDataCriacaoGreaterThan |
WHERE data_criacao > ? |
Like |
findByConteudoLike |
WHERE conteudo LIKE ? |
Containing |
findByConteudoContaining |
WHERE conteudo LIKE %?% |
StartingWith |
findByConteudoStartingWith |
WHERE conteudo LIKE ?% |
EndingWith |
findByConteudoEndingWith |
WHERE conteudo LIKE %? |
IsNull / IsNotNull |
findByConteudoIsNull |
WHERE conteudo IS NULL |
In |
findByidUsuarioIn |
WHERE id_usuario IN (?) |
OrderBy |
findByidUsuarioOrderByDataCriacaoDesc |
WHERE id_usuario = ? ORDER BY data_criacao DESC |
Top / First |
findTop5ByOrderByDataCriacaoAsc |
ORDER BY data_criacao ASC LIMIT 5 |
Exemplos de métodos em um repositório:
public interface RepositorioMensagemChat
extends JpaRepository<MensagemChat, Long> {
// Buscar mensagens por ID de usuário
List<MensagemChat> findByIdUsuario(Integer idUsuario);
// Buscar mensagens por ID de usuário e conteúdo contendo um termo
List<MensagemChat> findByIdUsuarioAndConteudoContaining(
Integer idUsuario, String termo);
// Buscar mensagens em um intervalo de tempo
List<MensagemChat> findByDataCriacaoBetween(
LocalDateTime inicio, LocalDateTime fim);
// Buscar mensagens com múltiplos critérios e ordenar por data de criação decrescente
List<MensagemChat> findByIdUsuarioAndConteudoContainingOrderByDataCriacaoDesc(
Integer idUsuario, String termo);
// Obter as 10 mensagens mais recentes
List<MensagemChat> findTop10ByOrderByDataCriacaoDesc();
}
- Consultas JPQL com Anotação @Query
Para consultas mais complexas que não podem ser expressas por nomes de métodos, a anotação @Query permite escrever JPQL (Java Persistence Query Language). JPQL opera sobre as entidades e seus atributos, não sobre as tabelas e colunas do banco de dados.
public interface RepositorioMensagemChat
extends JpaRepository<MensagemChat, Long> {
// Pesquisa por palavra-chave no conteúdo (JPQL usa '%:param%' diretamente)
@Query("SELECT mc FROM MensagemChat mc WHERE mc.conteudo LIKE %:palavraChave%")
List<MensagemChat> buscarPorPalavraChave(@Param("palavraChave") String palavraChave);
// Consulta multi-condicional com parâmetros opcionais e ordenação
@Query("SELECT mc FROM MensagemChat mc WHERE "
+ "(:idUser IS NULL OR mc.idUsuario = :idUser) "
+ "AND (:termo IS NULL OR mc.conteudo LIKE %:termo%) "
+ "ORDER BY mc.dataCriacao DESC")
List<MensagemChat> buscarPorUsuarioEConteudo(
@Param("idUser") Integer idUser,
@Param("termo") String termo);
// Consulta por intervalo de datas
@Query("SELECT mc FROM MensagemChat mc WHERE "
+ "mc.dataCriacao BETWEEN :dataInicial AND :dataFinal "
+ "ORDER BY mc.dataCriacao ASC")
List<MensagemChat> encontrarPorIntervaloDeData(
@Param("dataInicial") LocalDateTime dataInicial,
@Param("dataFinal") LocalDateTime dataFinal);
}
Observação: A sintaxe %:palavraChave% é específica do JPQL para concatenação de curingas. A condição (:param IS NULL OR entity.field = :param) é um padrão para tornar um parâmetro opcional; se o valor for null, a condição será ignorada.
- Consultas SQL Nativas com @Query
Quando há necessidade de utilizar funcionalidades específicas do banco de dados ou construir consultas que não se encaixam bem no JPQL, pode-se usar SQL nativo configurando nativeQuery = true na anotação @Query. Aqui, você trabalhará diretamente com os nomes das tabelas e colunas do seu esquema.
public interface RepositorioMensagemChat
extends JpaRepository<MensagemChat, Long> {
// Pesquisa nativa por palavra-chave
@Query(value = "SELECT * FROM mensagem_chat WHERE conteudo LIKE CONCAT('%', :termoBusca, '%')",
nativeQuery = true)
List<MensagemChat> pesquisarNativo(@Param("termoBusca") String termoBusca);
// Consulta por data específica (apenas ano-mês-dia) usando função de data do banco
@Query(value = "SELECT * FROM mensagem_chat "
+ "WHERE DATE(data_criacao) = :dataSelecionada "
+ "ORDER BY data_criacao DESC",
nativeQuery = true)
List<MensagemChat> encontrarPorDia(@Param("dataSelecionada") String dataSelecionada);
// Exemplo de uso: repo.encontrarPorDia("2026-04-24")
// Busca avançada com múltiplos critérios opcionais e ordenação (SQL nativo)
@Query(value = "SELECT * FROM mensagem_chat WHERE 1=1 "
+ "AND (:idUsuario IS NULL OR id_usuario = :idUsuario) "
+ "AND (:textoBusca IS NULL OR conteudo LIKE CONCAT('%', :textoBusca, '%')) "
+ "AND (:inicioPeriodo IS NULL OR data_criacao >= :inicioPeriodo) "
+ "AND (:fimPeriodo IS NULL OR data_criacao <= :fimPeriodo) "
+ "ORDER BY data_criacao DESC",
nativeQuery = true)
List<MensagemChat> buscaAvancada(
@Param("idUsuario") Integer idUsuario,
@Param("textoBusca") String textoBusca,
@Param("inicioPeriodo") LocalDateTime inicioPeriodo,
@Param("fimPeriodo") LocalDateTime fimPeriodo);
}
Importante: Em SQL nativo, a concatenação de curingas para LIKE geralmente exige a função CONCAT (ex: CONCAT('%', :param, '%')), ao contrário da sintaxe direta %:param% do JPQL.
- Specifications para Consultas Dinâmicas
A API Criteria, exposta via JpaSpecificationExecutor, é ideal para construir consultas dinâmicas onde os critérios de busca não são fixos e podem variar em tempo de execução. Para utilizá-la, seu repositório deve estender JpaSpecificationExecutor.
public interface RepositorioMensagemChat
extends JpaRepository<MensagemChat, Long>,
JpaSpecificationExecutor<MensagemChat> {
// Ao estender JpaSpecificationExecutor, métodos como findAll(Specification)
// e findAll(Specification, Pageable) ficam disponíveis.
}
Exemplo de um método de serviço que constrói uma Specification dinamicamente:
// Dentro de um serviço ou componente similar
public List<MensagemChat> consultarDinamico(Integer idUsuario, String termo,
LocalDateTime inicio, LocalDateTime fim) {
Specification<MensagemChat> especificacao = (Root<MensagemChat> root,
CriteriaQuery<?> query,
CriteriaBuilder cb) -> {
List<Predicate> condicoes = new ArrayList<>();
// Condição opcional: ID do usuário
if (idUsuario != null) {
condicoes.add(cb.equal(root.get("idUsuario"), idUsuario));
}
// Condição de busca parcial (LIKE)
if (termo != null && !termo.isEmpty()) {
condicoes.add(cb.like(root.get("conteudo"),
"%" + termo + "%"));
}
// Filtro por intervalo de datas
if (inicio != null) {
condicoes.add(cb.greaterThanOrEqualTo(
root.get("dataCriacao"), inicio));
}
if (fim != null) {
condicoes.add(cb.lessThanOrEqualTo(
root.get("dataCriacao"), fim));
}
// Definir ordenação (decrescente pela data de criação)
query.orderBy(cb.desc(root.get("dataCriacao")));
// Combinar todas as condições com AND
return cb.and(condicoes.toArray(new Predicate[0]));
};
return repo.findAll(especificacao);
}
- Paginação de Resultados
A paginação é essencial para lidar com grandes volumes de dados. Spring Data JPA simplifica isso através da interface Pageable.
public Page<MensagemChat> buscarComPaginacao(
Integer idUsuario, String termo,
LocalDateTime inicio, LocalDateTime fim,
int numeroPagina, int tamanhoPagina) {
Pageable informacoesPagina = PageRequest.of(numeroPagina, tamanhoPagina,
Sort.by(Sort.Direction.DESC, "dataCriacao"));
// Exemplo 1: Paginação com método derivado
// Necessário um método como: Page<MensagemChat> findByIdUsuario(Integer idUsuario, Pageable pageable);
// return repo.findByIdUsuario(idUsuario, informacoesPagina);
// Exemplo 2: Paginação com Specification (usando o exemplo anterior)
Specification<MensagemChat> especificacao = (Root<MensagemChat> root,
CriteriaQuery<?> query,
CriteriaBuilder cb) -> {
List<Predicate> condicoes = new ArrayList<>();
if (idUsuario != null) condicoes.add(cb.equal(root.get("idUsuario"), idUsuario));
if (termo != null && !termo.isEmpty()) condicoes.add(cb.like(root.get("conteudo"), "%" + termo + "%"));
if (inicio != null) condicoes.add(cb.greaterThanOrEqualTo(root.get("dataCriacao"), inicio));
if (fim != null) condicoes.add(cb.lessThanOrEqualTo(root.get("dataCriacao"), fim));
query.orderBy(cb.desc(root.get("dataCriacao")));
return cb.and(condicoes.toArray(new Predicate[0]));
};
return repo.findAll(especificacao, informacoesPagina);
// Exemplo 3: Paginação com @Query
// Necessário um método como: @Query("SELECT mc FROM MensagemChat mc WHERE mc.conteudo LIKE %:kw%") Page<MensagemChat> pesquisarPorTermoComPagina(@Param("kw") String kw, Pageable p);
// return repo.pesquisarPorTermoComPagina(termo, informacoesPagina);
}
Exemplo de uso em um controlador REST:
// GET /api/mensagens?idUsuario=1&termo=olá&pagina=0&tamanho=10
@RestController
@RequestMapping("/api/mensagens")
public class MensagemChatController {
@Autowired
private ServicoMensagemChat servico;
@GetMapping
public Page<MensagemChat> listarMensagens(
@RequestParam(required = false) Integer idUsuario,
@RequestParam(required = false) String termo,
@RequestParam(defaultValue = "0") int pagina,
@RequestParam(defaultValue = "10") int tamanho) {
return servico.buscarComPaginacao(idUsuario, termo, null, null, pagina, tamanho);
}
}
- Funções e Conversão de Datas
Manipular datas em consultas é uma necessidade comum. Veja algumas abordagens:
| Cenário | Exemplo de Implementação |
|---|---|
| Agrupar e contar por dia (SQL Nativo) | @Query(value = "SELECT DATE(data_criacao), COUNT(*) FROM mensagem_chat GROUP BY DATE(data_criacao) ORDER BY DATE(data_criacao) DESC", nativeQuery = true) List<Object[]> contarPorDia(); |
| Formatar data para string (SQL Nativo) | @Query(value = "SELECT DATE_FORMAT(data_criacao, '%Y-%m-%d') FROM mensagem_chat", nativeQuery = true) List<String> listarDatasFormatadas(); |
| Comparar apenas o dia (JPQL com FUNCTION) | @Query("SELECT mc FROM MensagemChat mc WHERE FUNCTION('DATE', mc.dataCriacao) = :data") List<MensagemChat> encontrarPorDiaJPQL(@Param("data") LocalDate data); |
| Consultar por ano e mês específicos (SQL Nativo) | @Query(value = "SELECT * FROM mensagem_chat WHERE YEAR(data_criacao) = :ano AND MONTH(data_criacao) = :mes", nativeQuery = true) List<MensagemChat> encontrarPorAnoMes(@Param("ano") int ano, @Param("mes") int mes); |
- Exemplo Abrangente: Busca Complexa com Múltiplos Critérios e Specification
Este exemplo demonstra um serviço que realiza uma busca combinada, onde uma palavra-chave pode corresponder tanto ao ID do usuário (se for um número) quanto ao conteúdo da mensagem, além de filtros por intervalo de tempo e ordenação.
@Service
public class ServicoMensagemChat {
@Autowired
private RepositorioMensagemChat repositorio;
/**
* Realiza uma busca complexa: palavra-chave correspondendo a idUsuario ou conteudo,
* mais filtro por intervalo de datas e ordenação decrescente.
*/
public List<MensagemChat> realizarBuscaComplexa(
String palavraChave,
LocalDateTime inicioPeriodo,
LocalDateTime fimPeriodo) {
Specification<MensagemChat> especificacaoBusca = (root, query, cb) -> {
List<Predicate> criterios = new ArrayList<>();
// Lógica para palavra-chave: buscar em 'conteudo' ou 'idUsuario' (se for número)
if (palavraChave != null && !palavraChave.isEmpty()) {
String padrao = "%" + palavraChave + "%";
Predicate condicaoConteudo = cb.like(root.get("conteudo"), padrao);
Predicate condicaoIdUsuario = null;
try {
Integer idValor = Integer.parseInt(palavraChave);
condicaoIdUsuario = cb.equal(root.get("idUsuario"), idValor);
} catch (NumberFormatException e) {
// Ignora, a palavra-chave não é um número para idUsuario
}
if (condicaoIdUsuario != null) {
criterios.add(cb.or(condicaoConteudo, condicaoIdUsuario));
} else {
criterios.add(condicaoConteudo);
}
}
// Filtro por intervalo de tempo
if (inicioPeriodo != null) {
criterios.add(cb.greaterThanOrEqualTo(
root.get("dataCriacao"), inicioPeriodo));
}
if (fimPeriodo != null) {
criterios.add(cb.lessThanOrEqualTo(
root.get("dataCriacao"), fimPeriodo));
}
// Ordenação por data de criação em ordem decrescente
query.orderBy(cb.desc(root.get("dataCriacao")));
return cb.and(criterios.toArray(new Predicate[0]));
};
return repositorio.findAll(especificacaoBusca);
}
}
- Resumo de Problemas Comuns
| Problema Comum | Solução |
|---|---|
| Consulta de texto parcial (LIKE) | Métodos derivados: Containing JPQL: LIKE %:param% SQL Nativo: LIKE CONCAT('%', :param, '%') Specification: cb.like(root.get("campo"), "%" + termo + "%") |
| Parâmetros opcionais (ignorados se null) | JPQL: (:param IS NULL OR campo = :param) Specification: Use blocos if (param != null) { ... } para adicionar condições dinamicamente. |
| Comparar apenas o dia/mês/ano de uma data | SQL Nativo: DATE(coluna_data) = :data, YEAR(coluna_data) = :ano, MONTH(coluna_data) = :mes ou DATE_FORMAT. JPQL: FUNCTION('DATE', entidade.dataCampo) = :data. |
| Ordenação dinâmica | Com Specification, use query.orderBy(cb.desc(root.get("campo"))). Com Pageable, use PageRequest.of(pagina, tamanho, Sort.by(Direction.DESC, "campo")). |
| Paginação combinada com filtros | Crie um objeto Pageable e passe-o para o método findAll(Specification, Pageable) do repositório, ou para um método derivado/@Query que aceite Pageable. |