Construindo Queries Dinâmicas com Specifications no Spring JPA

O Spring Data JPA fornece uma forma poderosa e flexível de criar consultas dinâmicas sem precisar escrever queries SQL manualmente. Uma das abordagens mais eficazes para isso é o uso da interface Specification, que permite construir filtros dinâmicos de forma programática.

Neste artigo, vamos aprender como usar o Spring JPA Specifications para criar consultas dinâmicas de maneira prática e eficiente.

O que são Specifications?

A interface Specification<T> faz parte do Spring Data JPA e permite construir consultas dinâmicas utilizando a API de Criteria do JPA. Com isso, podemos definir filtros de busca que podem ser combinados de forma modular, permitindo que a aplicação tenha consultas flexíveis.

Criando um exemplo prático

Vamos exemplificar como criar essas consultas, iremos utilizar de exemplo uma entidade produto, onde vamos pesquisar, por nome, preço e categoria.

@Entity
@Table(name = "produto")
public class Produto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String nome;
    private Double preco;
    private String categoria;
    
    // Getters e Setters
}
Produto.java

Agora que temos nossa entidade, vamos criar nossa interface de repositório.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface ProdutoRepository extends JpaRepository<Produto, Long>, JpaSpecificationExecutor<Produto> {

}
ProdutoRepository.java

Um ponto importante de se considerar é que como vamos utilizar specifications, é necessário estender mais uma interface JpaSpecificationExecutor<T> pois ela nos fornecerá os métodos necessários para trabalhar com as nossas especificações.

Agora vamos criar uma classe que concentra as nossas specífications.

import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;

public class ProdutoSpecification {

    public static Specification<Produto> filtrarPorNome(String nome) {
        return (root, query, criteriaBuilder) -> {
            if(nome != null && !nome.isEmpty()) {
                return criteriaBuilder.like(root.get("nome"), "%" + nome + "%");
            }
            return criteriaBuilder.conjunction();
        };
    }

    public static Specification<Produto> filtrarPorPrecoMin(Double precoMax) {
        return (root, query, criteriaBuilder) -> {
            if(precoMin != null) {
                return criteriaBuilder.greaterThanOrEqualTo(root.get("preco"), precoMin);
            }
            return criteriaBuilder.conjunction();
        };
    }

    public static Specification<Produto> filtrarPorPrecoMax(Double precoMax) {
        return (root, query, criteriaBuilder) -> {
            if(precoMin != null) {
                return criteriaBuilder.lessThanOrEqualTo(root.get("preco"), precoMax);
            }
            return criteriaBuilder.conjunction();
        };
    }

    public static Specification<Produto> filtrarPorCategoria(String categoria) {
        return (root, query, criteriaBuilder) -> {
            if(categoria != null && !categoria.isEmpty()) {
                return criteriaBuilder.equal(root.get("categoria"), categoria);
            }
            return criteriaBuilder.conjunction();
        };
    }

}
ProdutoSpecification.java

Importante notar, que preferimos uma abordagem onde cada parâmetro da consulta foi criado separado, permitindo assim a reutilização desses mesmos métodos em outras consultas, selecionando os que forem mais adequados.

Agora precisamos criar um serviço que utilize essas specifications juntamente com o nosso repositório, e vamos também criar uma classe que concentra os nossos parâmetros de filtro para deixar o código mais legível.

public class ProdutoFiltro {
    private String nome;
    private Double precoMin;
    private Double precoMax;
    private String categoria;
    
    //getters and setters
}
ProdutoFiltro.java

Agora nossa classe de serviço:

import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class ProdutoService {

    private final ProdutoRepository produtoRepository;

    public ProdutoService(ProdutoRepository produtoRepository) {
        this.produtoRepository = produtoRepository;
    }

    public List<Produto> buscarProdutos(ProdutoFiltro filtro) {

        Specification<Produto> spec = Specification
            .where(ProdutoSpecification.filtrarPorNome(filtro.getNome()))
            .and(ProdutoSpecification.filtrarPorPrecoMin(filtro.getPrecoMin()))
            .and(ProdutoSpecification.filtrarPorPrecoMax(filtro.getPrecoMax()))
            .and(ProdutoSpecification.filtrarPorCategoria(filtro.getCategoria()));

        return produtoRepository.findAll(spec);
    }
}
ProdutoService.java

E para testar tudo isso, vamos criar um controller.

import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/produtos")
public class ProdutoController {

    private final ProdutoService produtoService;

    public ProdutoController(ProdutoService produtoService) {
        this.produtoService = produtoService;
    }

    @GetMapping
    public ResponseEntity<List<Produto>> filtrarProdutos(ProdutoFiltro filtro) {
        List<Produto> produtos = produtoService.buscarProdutos(filtro);
        return ResponseEntity.status(HttpStatus.OK).body(produtos);    
    }
}
ProdutoController.java

Pronto, agora podemos testar nossa aplicação, nessa lógica que construímos os parâmetros só serão considerados para query quando eles forem passados, como adicionamos uma classe inteira como parâmetro para nosso método do controller eles deveram ser passados como query parameters. Ex:

curl --request GET --url http://localhost:8080/produtos?nome=ProdutoExemplo&precoMin=25&precoMax=50&categoria=games
Terminal

Usando specifications com paginação

Quando se usa specifications fica fácil de fazer as queries dinâmicas de forma paginada, pois ela se integra perfeitamente com a API de paginação que os Spring JPA nos entrega por padrão, vamos exemplificar utilizando o mesmo exemplo acima:

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.domain.PageRequest;
import org.springframework.data.jpa.domain.Page;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class ProdutoService {

    private final ProdutoRepository produtoRepository;

    public ProdutoService(ProdutoRepository produtoRepository) {
        this.produtoRepository = produtoRepository;
    }

    public Page<Produto> buscarProdutos(ProdutoFiltro filtro, Integer pagina, Integer quantidadePorPagina) {

        Specification<Produto> spec = Specification
            .where(ProdutoSpecification.filtrarPorNome(filtro.getNome()))
            .and(ProdutoSpecification.filtrarPorPrecoMin(filtro.getPrecoMin()))
            .and(ProdutoSpecification.filtrarPorPrecoMax(filtro.getPrecoMax()))
            .and(ProdutoSpecification.filtrarPorCategoria(filtro.getCategoria()));

        return produtoRepository.findAll(spec, PageRequest.of(pagina, quantidadePorPagina));
    }
}
ProdutoService.java

Métodos da interface Specification<T>

No exemplo acima, conseguimos mostrar alguns métodos de comparação e também de operação lógica quando juntamos algumas specifications, mas essa API entrega muitos outros, tornando possível qualquer tipo de consulta que seria feito com SQL nativo.

Métodos de Comparação

  • equal(Expression x, Expression y): Verifica se dois valores são iguais.
  • equal(Expression x, Object y): Compara uma expressão com um valor específico.
  • notEqual(Expression x, Expression y): Verifica se dois valores são diferentes.
  • notEqual(Expression x, Object y): Compara uma expressão com um valor específico e verifica se são diferentes.
  • greaterThan(Expression x, Expression y): Verifica se um valor é maior que outro.
  • greaterThan(Expression x, Object y): Compara um valor com outro.
  • greaterThanOrEqualTo(Expression x, Expression y): Verifica se um valor é maior ou igual a outro.
  • greaterThanOrEqualTo(Expression x, Object y): Compara um valor com outro.
  • lessThan(Expression x, Expression y): Verifica se um valor é menor que outro.
  • lessThan(Expression x, Object y): Compara um valor com outro.
  • lessThanOrEqualTo(Expression x, Expression y): Verifica se um valor é menor ou igual a outro.
  • lessThanOrEqualTo(Expression x, Object y): Compara um valor com outro.

Métodos de Operações Lógicas

  • and(Predicate… restrictions): Constrói uma expressão AND.
  • or(Predicate… restrictions): Constrói uma expressão OR.
  • not(Expression<Boolean> expression): Inverte uma expressão booleana.

Métodos de Condições de Intervalo

  • between(Expression<? extends Comparable> v, Expression<? extends Comparable> x, Expression<? extends Comparable> y): Verifica se um valor está entre dois outros.
  • between(Expression<? extends Comparable> v, Object x, Object y): Compara se um valor está dentro de um intervalo.

Métodos de Condições de Texto

  • like(Expression<String> x, String pattern): Verifica se uma string corresponde a um padrão usando LIKE.
  • like(Expression<String> x, String pattern, char escapeChar): Permite definir um caractere de escape para o LIKE.
  • notLike(Expression<String> x, String pattern): Verifica se uma string não corresponde a um padrão.
  • notLike(Expression<String> x, String pattern, char escapeChar): Mesma funcionalidade, mas com caractere de escape.

Métodos de Presença/Nulidade

  • isNull(Expression<?> x): Verifica se um valor é NULL.
  • isNotNull(Expression<?> x): Verifica se um valor não é NULL.

Métodos de Listas e Coleções

  • in(Expression<?> x): Cria um critério de verificação de pertencimento a uma coleção.
  • isMember(Object element, Expression<Collection<?>> collection): Verifica se um elemento pertence a uma coleção.
  • isNotMember(Object element, Expression<Collection<?>> collection): Verifica se um elemento não pertence a uma coleção.

Métodos de Ordenação

  • asc(Expression<?> x): Define uma ordenação ascendente.
  • desc(Expression<?> x): Define uma ordenação descendente.

Métodos de Funções Agregadas

  • count(Expression<?> x): Conta o número de registros.
  • countDistinct(Expression<?> x): Conta o número de registros distintos.
  • sum(Expression<N> x): Calcula a soma dos valores de uma coluna.
  • avg(Expression<N> x): Calcula a média dos valores de uma coluna.
  • max(Expression<N> x): Retorna o valor máximo de uma coluna.
  • min(Expression<N> x): Retorna o valor mínimo de uma coluna.

Conclusão

O uso de Specifications no Spring JPA permite a criação de consultas dinâmicas de forma modular e flexível, sem a necessidade de escrever queries SQL manualmente. Isso melhora a manutenção do código e facilita a implementação de filtros personalizados.

Agora você pode aplicar essa abordagem em seus projetos e criar buscas avançadas de maneira eficiente!

Links úteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 72