Texto de: Geraldo Daros
Introdução
Em aplicações com Spring Boot é comum que precisemos realizar consultas complexas e dinâmicas no banco de dados e, muitas vezes, as queries básicas oferecidas pelo Spring Data JPA não são suficientes para atender a requisitos específicos da aplicação.
O Spring Data JPA fornece uma abordagem para resolver esse problema por meio da interface Specification
. A Specification
permite que você construa consultas dinâmicas com base em critérios fornecidos em tempo de execução. Neste artigo, vamos explorar como aplicar Specification
em um projeto Spring Boot com um CRUD de produtos.
Projeto
Para começarmos, vamos usar o site https://start.spring.io/ para gerarmos um projeto Spring Boot. Vamos adicionar dependências de Spring Web, Spring Data JPA e do banco de dados em memória H2 Database. Você pode optar por usar qualquer outro banco de dados, mas nesse artigo abordaremos apenas o processo de configuração do H2. Também é importante avisar que nesse exemplo será usado a versão 17 do Java e o Maven.
Depois de gerar o projeto, você já poderá abri-lo em uma IDE de sua escolha. Depois disso, você pode colocar o projeto para rodar. O mesmo deve rodar sem problemas, e estar disponível na porta 8080.
Código
O código referente as operações básicas de CRUD não será abordado em detalhes neste artigo, uma vez que o foco deste é o uso de Specifications
e saber Spring Boot e Java é um pré-requisito para compreender esse conceito.
Entidade Produto (criada em um pacote chamado produto):
package com.example.demo.produto;
import jakarta.persistence.*;
@Entity
public class Produto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nome;
private double preco;
private String categoria;
// Construtor padrão e com todos parâmetros
// Getters e Setters
}
Repositório:
package com.example.demo.produto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface ProdutoRepository extends JpaRepository<Produto, Long>, JpaSpecificationExecutor<Produto> {
}
Nesse momento você já notou que a classe ProdutoRepository está estendendo JpaRepositoy
e também de JpaSpecification
. Ao estender JpaSpecificationExecutor
, o repositório ganha suporte a consultas baseadas em Specification
. Ainda nos faltam os códigos do controller e da service, mas vamos deixar isso para depois.
Specification
Agora, vamos criar uma nova classe. Por convenção, fazemos a nomeação com o nome da entidade + specification
, então o nome que usarei será ProdutoSpecification
.
Nessa classe criaremos um método público e estático que retornara uma Specification de Produto com qualquer nome. Vou chamar esse método de temNome e adicionar o conteúdo abaixo:
public static Specification<Produto> temNome(String nome) {
return (root, query, criteriaBuilder) ->
nome == null ? null : criteriaBuilder.equal(root.get("nome"), nome);
}
Vamos revisar tudo que está acontecendo aqui:
root.get("nome")
: Esse trecho acessa o camponome
da entidadeProduto
. Ele é equivalente a referenciar a colunanome
na tabela de banco de dados.criteriaBuilder.equal(root.get("nome"), nome)
: Isso cria uma condição (Predicate
) que compara o valor da propriedadenome
no banco de dados com o valor do parâmetronome
. Senome
fornull
, aSpecification
retornanull
, indicando que esse critério não deve ser aplicado.- Retorno (
return (root, query, criteriaBuilder) -> ...
): O método retorna umaSpecification
que pode ser usada no métodofindAll()
do repositório. Quando essePredicate
é avaliado, ele é traduzido para uma cláusula SQL comoWHERE nome = ?
ou algo equivalente, dependendo do banco de dados em uso. O parâmetroroot
representa a entidade de origem para a qual a consulta está sendo criada enquantoquery
é a própria consulta que está sendo construída ecriteriaBuilder
é uma fábrica de critérios que permite criar as condições (Predicate
) que serão aplicadas à consulta. Esses três parâmetros (root
,query
,criteriaBuilder
) são gerados automaticamente pelo Spring Data JPA quando você chama o métodofindAll(Specification spec)
em um repositório que implementaJpaSpecificationExecutor
.
Consultando Dados
Agora que já criamos a Specification
, construiremos uma classe de Service para fazer uso do nosso repositório.
package com.example.demo.produto;
import java.util.List;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
@Service
public class ProdutoService {
private final ProdutoRepository productRepository;
public ProdutoService(ProdutoRepository productRepository) {
this.productRepository = productRepository;
}
public List<Produto> obterProdutos(String name) {
Specification<Produto> specification = Specification.where(ProdutoSpecification.temNome(name));
return productRepository.findAll(specification);
}
public void salvar(Produto produto) {
productRepository.save(produto);
}
}
E também criaremos nossa Controller:
package com.example.demo.produto;
import java.util.List;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/produtos")
public class ProdutoController {
private final ProdutoService produtoService;
public ProdutoController(ProdutoService produtoService) {
this.produtoService = produtoService;
}
@GetMapping
public List<Produto> buscarProdutos(
@RequestParam(required = false) String nome) {
return produtoService.obterProdutos(nome);
}
@PostMapping
public void salvar(@RequestBody Produto produto) {
produtoService.salvar(produto);
}
}
Nesse momento nosso projeto está quase pronto para fazer o uso das Specifications
, basta configurarmos nosso banco de dados no arquivo properties, conforme feito abaixo.
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
Também será necessário inserir alguns dados na aplicação, como descrito abaixo:
Envia para: POST <http://localhost:8080/produtos>
{
"nome": "Melancia",
"preco": 2.99,
"categoria": "Hortifruti"
}
Após adicionar alguns itens, podemos fazer a consulta:
<http://localhost:8080/produtos?nome=Melancia>
Se você não passar nenhum parâmetro na URL, todos os itens serão buscados. A consulta usada acima deve ter no parâmetro nome um valor igual ao nome criado anteriormente.
Criando mais Speficiations
Agora vamos fazer mais 2 tipos de consultas e criar Specifications para essas consultas:
- Criaremos uma Specification para fazer consulta pela categoria.
- Criaremos uma Specification para procurar por preço menor ou igual.
Para isso, adicionaremos esses dois métodos a nossa classe ProdutoSpecification:
public static Specification<Produto> temCategoria(String categoria) {
return (root, query, criteriaBuilder) ->
categoria == null ? null : criteriaBuilder.equal(root.get("categoria"), categoria);
}
public static Specification<Produto> temPrecoMenorOuIgual(Double preco) {
return (root, query, criteriaBuilder) ->
preco == null ? null : criteriaBuilder.lessThanOrEqualTo(root.get("preco"), preco);
}
Note que estes métodos seguem o mesmo padrão que o primeiro método. Apenas alteramos o método do criteriaBuilder que vamos usar, e claro, o parâmetro recebido.
Agora precisamos ajustar o Controller. Para isso, adicionaremos mais dois parâmetros no método get, deixamos eles opcionais e os passaremos para a Service.
@GetMapping
public List<Produto> buscarProdutos(
@RequestParam(required = false) String nome,
@RequestParam(required = false) String categoria,
@RequestParam(required = false) Double preco) {
return produtoService.obterProdutos(nome, categoria, preco);
}
Da mesma forma, precisamos preparar a nossa Service para receber os parâmetros adicionais, e passamos para o repositório como forma de Specification
, adicionando o and para adicionar mais uma Specification se necessário.
public List<Produto> obterProdutos(String name, String categoria, Double preco) {
Specification<Produto> specification = Specification.where(ProdutoSpecification.temNome(name)
.and(ProdutoSpecification.temCategoria(categoria)
.and(ProdutoSpecification.temPrecoMenorOuIgual(preco))));
return productRepository.findAll(specification);
}
Pronto, agora é só testar!
Com essas Specifications
feitas, você pode fazer um filtro, e outro, e outro, para buscar apenas o que é realmente relevante para a consulta de forma otimizada e simples, exemplo: http://localhost:8080/produtos?categoria=Hortifruti&preco=2
Conclusão
O uso de Specifications
no Spring Boot permite flexibilidade na construção de consultas complexas e dinâmicas, simplificando a lógica de pesquisa. Isso facilita a manutenção e extensão do código à medida que novos critérios de busca são adicionados.