Spring Boot 2.2.1 com MyBatis-Plus: Controle de Versão, Consultas Avançadas, Paginação e Exclusão Lógica

Segunda Parte: Mecanismo de Locking e Operações de Banco de Dados

Configuração do Ambiente de Teste

Primeiro, vamos criar uma tabela de banco de dados para simular um sistema de estoque de produtos.

CREATE TABLE produto (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(255) NOT NULL,
    quantidade INT NOT NULL,
    preco DECIMAL(10,2) NOT NULL,
    versao INT NOT NULL DEFAULT 0
);

Agora, criamos a classe de entidade que será mapeada pelo MyBatis-Plus:

import com.baomidou.mybatisplus.annotation.*;

@TableName("produto")
public class Produto {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String nome;
    private Integer quantidade;
    private BigDecimal preco;
    private Integer versao;
    
    // Getters e Setters omitidos para brevity
}

Criamos a interface Mapper que estende BaseMapper:

public interface ProdutoMapper extends BaseMapper<Produto> {
}

Cenário de Problema: Atualizações Concorrentes

Considere o seguinte cenário de teste:

@Test
public void testAtualizacaoConcorrente() {
    // Cliente A obtém os dados
    Produto prod1 = produtoMapper.selectById(1L);
    System.out.println("Cliente A consultou: " + prod1.getPreco());
    
    // Cliente B obtém os dados
    Produto prod2 = produtoMapper.selectById(1L);
    System.out.println("Cliente B consultou: " + prod2.getPreco());
    
    // Cliente A adiciona 50 ao preço
    prod1.setPreco(prod1.getPreco().add(new BigDecimal("50")));
    produtoMapper.updateById(prod1);
    
    // Cliente B subtrai 30 do preço
    prod2.setPreco(prod2.getPreco().subtract(new BigDecimal("30")));
    produtoMapper.updateById(prod2);
    
    // Resultado final
    Produto prod3 = produtoMapper.selectById(1L);
    System.out.println("Resultado final: " + prod3.getPreco());
}

O resultado será incorreto porque a segunda atualização sobrescreve a primeira. Este é um problema clássico de condição de corrida.

Solução 1: Lock Pessimista

O lock pessimista bloqueia o registro no banco de dados até que a transação seja concluída. Utilizamos a cláusula FOR UPDATE:

// Na interface Mapper
@Select("SELECT * FROM produto WHERE id = #{id} FOR UPDATE")
Produto bloquearProdutoParaAtualizacao(Long id);

// Teste com lock pessimista
@Test
public void testLockPessimista() {
    Produto prod = produtoMapper.bloquearProdutoParaAtualizacao(1L);
    
    // Modifica o preço
    prod.setPreco(prod.getPreco().add(new BigDecimal("50")));
    produtoMapper.updateById(prod);
    
    // O lock é liberado automaticamente após a transação
    
    Produto resultado = produtoMapper.selectById(1L);
    System.out.println("Preço final: " + resultado.getPreco());
}

Solução 2: Lock Otimista

O lock otimista utiliza um campo de versão para verificar se o registro foi modificado por outro processo antes de realizar a atualização.

Configuração do Plugin

package com.exemplo.configuracao;

import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement
@Configuration
public class ConfiguracaoMyBatisPlus {
    
    @Bean
    public OptimisticLockerInterceptor interceptorLockOtimista() {
        return new OptimisticLockerInterceptor();
    }
    
    @Bean
    public PaginationInterceptor interceptorPaginação() {
        return new PaginationInterceptor();
    }
}

Adicionando a Anotação @Version

@TableName("produto")
public class Produto {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String nome;
    private Integer quantidade;
    private BigDecimal preco;
    
    @Version
    private Integer versao;
}

Testando o Lock Otimista

@Test
public void testLockOtimista() {
    // Cliente A obtém os dados
    Produto prod1 = produtoMapper.selectById(1L);
    System.out.println("Cliente A - Versão: " + prod1.getVersao());
    
    // Cliente B obtém os dados
    Produto prod2 = produtoMapper.selectById(1L);
    System.out.println("Cliente B - Versão: " + prod2.getVersao());
    
    // Cliente A atualiza
    prod1.setPreco(prod1.getPreco().add(new BigDecimal("50")));
    int resultadoA = produtoMapper.updateById(prod1);
    System.out.println("Cliente A atualizou: " + resultadoA);
    
    // Cliente B tenta atualizar - deve falhar
    prod2.setPreco(prod2.getPreco().subtract(new BigDecimal("30")));
    int resultadoB = produtoMapper.updateById(prod2);
    
    if (resultadoB == 0) {
        System.out.println("Cliente B falhou - retry necessário");
        // Lógica de retry
        prod2 = produtoMapper.selectById(1L);
        prod2.setPreco(prod2.getPreco().subtract(new BigDecimal("30")));
        produtoMapper.updateById(prod2);
    }
    
    Produto prod3 = produtoMapper.selectById(1L);
    System.out.println("Resultado final: " + prod3.getPreco());
}

Construção de Consultas Complexas

Exepmlos de Operações CRUD

// Consulta com múltiplos registros
@Test
public void testConsultarListaProdutos() {
    QueryWrapper<Produto> wrapper = new QueryWrapper<>();
    wrapper.gt("preco", 50);
    List<Produto> produtos = produtoMapper.selectList(wrapper);
    produtos.forEach(p -> System.out.println(p.getId() + " - " + p.getNome()));
}

// Atualização em massa
@Test
public void testAtualizarListaProdutos() {
    UpdateWrapper<Produto> wrapper = new UpdateWrapper<>();
    wrapper.set("preco", new BigDecimal("200")).ge("preco", 100);
    int linhas = produtoMapper.update(new Produto(), wrapper);
    System.out.println("Linhas atualizadas: " + linhas);
}

// Consulta com condições complexas
@Test
public void testConsultaCondicaoComplexa() {
    QueryWrapper<Produto> wrapper = new QueryWrapper<>();
    wrapper.like("nome", "Produto").or().eq("preco", 100);
    List<Produto> produtos = produtoMapper.selectList(wrapper);
    produtos.forEach(p -> System.out.println(p.getId() + " - " + p.getNome()));
}

// Consulta com Join
@Test
public void testConsultaComCategoria() {
    List<Map<String, Object>> resultados = produtoMapper.selectMaps(
        new QueryWrapper<Produto>()
            .select("p.id as idProduto", "p.nome as nomeProduto", "p.preco", "c.nome as nomeCategoria")
            .leftJoin("categoria c ON c.id = p.categoria_id")
            .orderByAsc("p.id")
    );
    resultados.forEach(item -> System.out.println(item.get("idProduto")));
}

Tipos de Query Builders

O MyBatis-Plus oferece diversos builders para diferentes necessidades:

  • QueryWrapper: Construção de condições de consulta
  • UpdateWrapper: Construção de condições para atualização
  • LambdaQueryWrapper: Versão com Lambda para type-safety
  • LambdaUpdateWrapper: Versão Lambda para atualizações

Métodos de Condição Disponíveis

Método Descrição
eq() Igual a
ne() Diferente de
gt() Maior que
ge() Maior ou igual
lt() Menor que
le() Menor ou igual
between() Entre dois valores
like() Like com %valor%
in() Contido em lista
isNull() É nulo
isNotNull() Não é nulo
// Exemplo com QueryWrapper
QueryWrapper<Usuario> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("idade", 25)
            .like("nome", "João")
            .in("status", Arrays.asList(1, 2, 3));

List<Usuario> usuarios = usuarioMapper.selectList(queryWrapper);

// Exemplo com UpdateWrapper
UpdateWrapper<Usuario> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("idade", 25)
             .set("status", 1);

int linhas = usuarioMapper.update(new Usuario(), updateWrapper);

Implementação de Paginação

A paginação no MyBatis-Plus é implementada através do plugin de paginação configurado anteriormente.

@Test
public void testPaginaSimples() {
    Page<Usuario> pagina = new Page<>(1, 5);
    Page<Usuario> resultadoPagina = usuarioMapper.selectPage(pagina, null);
    
    List<Usuario> registros = resultadoPagina.getRecords();
    
    System.out.println("Total de páginas: " + resultadoPagina.getPages());
    System.out.println("Total de registros: " + resultadoPagina.getTotal());
    System.out.println("Página atual: " + resultadoPagina.getCurrent());
    System.out.println("Tamanho da página: " + resultadoPagina.getSize());
    System.out.println("Tem próxima página: " + resultadoPagina.hasNext());
    System.out.println("Tem página anterior: " + resultadoPagina.hasPrevious());
}

Para selecionar apenas campos específicos:

@Test
public void testPaginaComCamposEspecificos() {
    QueryWrapper<Usuario> wrapper = new QueryWrapper<>();
    wrapper.select("id", "nome");
    
    Page<Map<String, Object>> pagina = new Page<>(1, 5);
    Page<Map<String, Object>> resultado = usuarioMapper.selectMapsPage(pagina, wrapper);
    
    List<Map<String, Object>> registros = resultado.getRecords();
    registros.forEach(System.out::println);
}

Exclusão Física vs Exclusão Lógica

Exclusão Física (Hard Delete)

Remove permanentemente o registro do banco de dados:

// Excluir por ID
@Test
public void testExcluirPorId() {
    int resultado = usuarioMapper.deleteById(1L);
    System.out.println("Excluídas " + resultado + " linhas");
}

// Excluir múltiplos IDs
@Test
public void testExcluirMultiplos() {
    int resultado = usuarioMapper.deleteBatchIds(Arrays.asList(10, 11, 12));
    System.out.println("Excluídas " + resultado + " linhas");
}

// Excluir por condições
@Test
public void testExcluirPorCondicao() {
    HashMap<String, Object> mapa = new HashMap<>();
    mapa.put("nome", "Olá");
    mapa.put("idade", 18);
    int resultado = usuarioMapper.deleteByMap(mapa);
    System.out.println("Excluídas " + resultado + " linhas");
}

Exclusão Lógica (Soft Delete)

A exclusão lógica mantém o registrro no banco, mas o marca como excluído através de um campo flag. Isso é útil para auditoria e análise de dados históricos.

Passo 1: Adicionar campo na tabela

ALTER TABLE usuario ADD COLUMN excluido TINYINT DEFAULT 0;

Passo 2: Configurar a anotação na entidade

@Data
public class Usuario {
    private Long id;
    private String nome;
    private Integer idade;
    private String email;
    
    @TableField(fill = FieldFill.INSERT)
    private Date dataCriacao;
    
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date dataAtualizacao;
    
    @TableLogic
    private Integer excluido;
}

Com a anotação @TableLogic, o MyBatis-Plus会自动在 todas as operações de exclusão e consulta adicionar a condição WHERE excluido = 0, simulando a exclusão lógica.

Publicado em 7-3 21:44