Blog Formação DEV

Introdução ao uso de Specification no Spring Boot

Otimize e simplifique suas consultas ao banco de dados de forma simples e elegante com as Specifications
Introdução ao uso de Specification no Spring Boot
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 campo nome da entidade Produto. Ele é equivalente a referenciar a coluna nome na tabela de banco de dados.
  • criteriaBuilder.equal(root.get("nome"), nome): Isso cria uma condição (Predicate) que compara o valor da propriedade nome no banco de dados com o valor do parâmetro nome. Se nome for null, a Specification retorna null, indicando que esse critério não deve ser aplicado.
  • Retorno (return (root, query, criteriaBuilder) -> ...): O método retorna uma Specification que pode ser usada no método findAll() do repositório. Quando esse Predicate é avaliado, ele é traduzido para uma cláusula SQL como WHERE nome = ? ou algo equivalente, dependendo do banco de dados em uso. O parâmetro root representa a entidade de origem para a qual a consulta está sendo criada enquanto query é a própria consulta que está sendo construída e criteriaBuilder é 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étodo findAll(Specification spec) em um repositório que implementa JpaSpecificationExecutor.

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.

Sobre o autor
Cod3r

Cod3r

Com mais de 400 mil alunos, a Cod3r é uma das principais escolas de tecnologia do País. Um de seus produtos mais importantes é a Formação DEV, com objetivo de preparar os profissionais para o mercado.

Ótimo! Inscreveu-se com sucesso.

Bem-vindo de volta! Registou-se com sucesso.

Assinou com sucesso o Blog Formação DEV .

Sucesso! Verifique o seu e-mail para obter o link mágico para se inscrever.

As suas informações de pagamento foram atualizadas.

Seu pagamento não foi atualizado.