Consultas Avançadas com Spring Data JPA: Métodos Derivados, JPQL, SQL Nativo e Specifications

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.

  1. 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;
}

  1. 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();
}

  1. 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.

  1. 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.

  1. 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);
}

  1. 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);
    }
}

  1. 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);
  1. 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);
    }
}

  1. 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.

Tags: Spring Data JPA jpa JPQL SQL Nativo Specification

Publicado em 7-22 20:16