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.