Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
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.
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.
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.javaAgora 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.javaUm 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.javaImportante 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.javaAgora 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.javaE 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.javaPronto, 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
TerminalQuando 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.javaNo 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.
AND
.OR
.LIKE
.LIKE
.NULL
.NULL
.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!