MyBatis-Plus 高级特性全解析,从主键策略到乐观锁,搞定开发所有场景
·
一、主键生成策略:解决分布式 ID 问题
1.1 核心注解:@TableId
在实体类主键字段上添加 @TableId 注解,通过 type 属性指定主键生成策略,支持局部配置(优先级高) 和全局配置。
java
运行
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
@Data
public class User {
// 局部配置:指定主键策略为数据库自增
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
}
1.2 常用主键策略(IdType 枚举)
| IdType 值 | 描述 | 适用场景 |
|---|---|---|
| AUTO | 数据库 ID 自增 | 单库单表,MySQL 等支持自增 |
| INPUT | 手动输入主键值 | 主键由业务层自定义生成 |
| ASSIGN_ID | 雪花算法生成全局唯一 Long 型 ID(MP 默认) | 分布式系统,需全局唯一 ID |
| ASSIGN_UUID | 生成 32 位 UUID 字符串(无中划线) | 分布式系统,主键为字符串类型 |
| NONE | 未设置,跟随全局配置 | 统一使用项目全局策略 |
重点:雪花算法(ASSIGN_ID)
- 结构:64 位 Long 型 ID(1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位流水号);
- 优势:全局唯一、有序递增、性能高,支持 1024 台机器,每台每毫秒生成 4096 个 ID,可用约 69 年;
- 数据库要求:主键字段为 BIGINT 类型即可,无需自增。
1.3 全局配置(application.properties)
统一配置所有实体类的默认主键策略,无需每个实体类单独配置:
# 全局配置雪花算法(推荐分布式场景)
mybatis-plus:
global-config:
db-config:
id-type: assign_id
# 若全局配置数据库自增
# id-type: auto
1.4 实战示例
| 策略 | 配置方式 | 数据库要求 | 插入数据注意事项 |
|---|---|---|---|
| 数据库自增 | @TableId(type = IdType.AUTO) |
主键字段需设置 AUTO_INCREMENT | 无需手动 set 主键,MP 自动回填自增 ID |
| 雪花算法 | @TableId(type = IdType.ASSIGN_ID) |
主键为 BIGINT 类型 | 无需手动 set 主键,MP 自动生成并回填 |
| 手动输入 | @TableId(type = IdType.INPUT) |
无 | 必须手动 set 主键值,否则报主键为空错误 |
// 手动输入主键示例
@Test
public void insertInput() {
User user = new User();
user.setId(7L); // 手动设置主键
user.setName("李四");
user.setAge(18);
user.setEmail("lisi@qq.com");
userMapper.insert(user); // 插入数据
}
二、条件构造器 Wrapper:实现复杂条件查询
2.1 核心构造器:QueryWrapper
用于构建 SELECT/DELETE/UPDATE 的条件,通过链式调用拼接条件,无需手写 SQL,支持 Lambda 表达式避免字段名写错。
2.2 常用条件方法(对应 SQL)
| 方法 | 描述 | 对应 SQL |
|---|---|---|
| eq(column, val) | 等于 | column = val |
| ne(column, val) | 不等于 | column != val |
| gt/ge(column, val) | 大于 / 大于等于 | column > val / >= val |
| lt/le(column, val) | 小于 / 小于等于 | column < val / <= val |
| between(c, v1, v2) | 区间查询 | column between v1 and v2 |
| like(column, val) | 模糊查询 | column like %val% |
| likeRight(column, val) | 左模糊 | column like val% |
| isNotNull(column) | 字段不为空 | column is not null |
| inSql(column, sql) | 子查询 | column in (sql 语句) |
| orderByDesc(column) | 按字段倒序 | order by column desc |
2.3 多场景实战示例
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class WrapperTest {
@Autowired
private UserMapper userMapper;
// 示例1:姓名/邮箱不为空,年龄>=12
@Test
void query1() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age", 12);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
// 示例2:查询姓名为「李四」的单个用户(注意:结果超过1条会报错)
@Test
void query2() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "李四");
User user = userMapper.selectOne(wrapper);
System.out.println(user);
}
// 示例3:统计年龄20~30之间的用户数量
@Test
void query3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age", 20, 30);
Integer count = userMapper.selectCount(wrapper);
System.out.println("符合条件数量:" + count);
}
// 示例4:模糊查询(姓名不含e,邮箱以t开头)
@Test
void query4() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.notLike("name", "e")
.likeRight("email", "t");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
// 示例5:子查询(id >= 2)
@Test
void query5() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.inSql("id", "select id from user where id>=2");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
// 示例6:按id倒序查询
@Test
void query6() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.orderByDesc("id");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
}
三、内置分页插件:实现物理分页
3.1 步骤 1:配置分页拦截器
创建 MP 配置类,注入分页拦截器并指定数据库类型:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.core.toolkit.DbType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页拦截器,指定MySQL数据库
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
3.2 步骤 2:使用 Page 对象实现分页查询
核心是 Page<T> 对象,传入 selectPage 方法后自动返回分页结果(包含数据、总条数、总页码等)。
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class PageTest {
@Autowired
private UserMapper userMapper;
// 分页查询:第2页,每页3条数据
@Test
void pageQuery() {
// 创建Page对象:参数1=当前页码,参数2=每页条数
Page<User> page = new Page<>(2, 3);
// selectPage:参数1=Page对象,参数2=条件构造器(null=无条件)
IPage<User> userPage = userMapper.selectPage(page, null);
// 获取分页结果
List<User> records = userPage.getRecords(); // 当前页数据
long total = userPage.getTotal(); // 总记录数
long current = userPage.getCurrent(); // 当前页码
long pages = userPage.getPages(); // 总页码
boolean hasNext = userPage.hasNext(); // 是否有下一页
boolean hasPrev = userPage.hasPrevious(); // 是否有上一页
// 输出结果
System.out.println("当前页数据:");
records.forEach(System.out::println);
System.out.println("总记录数:" + total + ",当前页:" + current + ",总页码:" + pages);
System.out.println("是否有下一页:" + hasNext + ",是否有上一页:" + hasPrev);
}
}
3.3 分页结果核心方法(IPage<T>)
表格
| 方法 | 功能 |
|---|---|
| getRecords() | 获取当前页数据列表 |
| getTotal() | 获取总记录数 |
| getCurrent() | 获取当前页码 |
| getPages() | 获取总页码 |
| hasNext() | 是否有下一页 |
| hasPrevious() | 是否有上一页 |
四、逻辑删除:防止误删,数据可恢复
4.1 核心原理
- 数据库添加逻辑删除字段(如
deleted),默认 0(未删除),删除时更新为 1(已删除); - 调用 MP 删除方法时,自动转为更新操作;
- 调用查询方法时,自动过滤
deleted=1的数据。
4.2 实战步骤
步骤 1:数据库添加逻辑删除字段
sql
-- 给user表添加deleted字段,默认0(未删除)
ALTER TABLE user ADD COLUMN deleted INT DEFAULT 0 COMMENT '逻辑删除:0=未删除,1=已删除';
步骤 2:实体类添加 @TableLogic 注解
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
@Data
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
@TableLogic // 标识为逻辑删除字段
private Integer deleted;
}
步骤 3:自定义逻辑删除值(可选)
properties
mybatis-plus:
global-config:
db-config:
logic-not-delete-value: 0 # 未删除值(默认0)
logic-delete-value: 1 # 已删除值(默认1)
步骤 4:测试逻辑删除
@Test
void logicDelete() {
// 调用deleteById,实际执行UPDATE操作
userMapper.deleteById(1L);
}
执行结果(SQL):
sql
UPDATE user SET deleted=1 WHERE id=? AND deleted=0
五、乐观锁:解决并发更新冲突
5.1 核心概念
表格
| 锁类型 | 核心思想 | 适用场景 |
|---|---|---|
| 乐观锁 | 无锁,更新时判断版本号 | 读多写少 |
| 悲观锁 | 操作前加锁,阻塞其他线程 | 写多读少 |
MP 乐观锁原理:通过版本号(version)字段控制,更新时判断版本号是否一致,一致则更新(版本号 + 1),不一致则更新失败。
5.2 实战步骤(商品价格并发更新)
步骤 1:创建商品表并插入测试数据
sql
CREATE TABLE product (
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称',
price INT(11) DEFAULT 0 COMMENT '价格',
version INT(11) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id)
);
-- 插入测试数据:外星人笔记本,价格1000,版本号0
INSERT INTO product (id, name, price) VALUES (1, '外星人笔记本', 1000);
步骤 2:商品实体类添加 @Version 注解
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
@Data
public class Product {
@TableId
private Long id;
private String name;
private Integer price;
@Version // 标识为乐观锁版本号字段
private Integer version;
}
步骤 3:配置乐观锁拦截器
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.core.toolkit.DbType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁拦截器
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
步骤 4:编写商品 Mapper 接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mp.pojo.Product;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
步骤 5:模拟并发更新测试
无锁测试(数据覆盖)
@Test
void testProductNoLock() {
// 1.小李获取商品价格
Product productLi = productMapper.selectById(1);
System.out.println("小李获取的价格:" + productLi.getPrice()); // 1000
// 2.小王获取商品价格
Product productWang = productMapper.selectById(1);
System.out.println("小王获取的价格:" + productWang.getPrice()); // 1000
// 3.小李加500并更新
productLi.setPrice(productLi.getPrice() + 500);
productMapper.updateById(productLi); // 价格1500,版本号仍0
// 4.小王减300并更新(覆盖数据)
productWang.setPrice(productWang.getPrice() - 300);
productMapper.updateById(productWang); // 价格700
// 5.老板查询
Product productBoss = productMapper.selectById(1);
System.out.println("老板看到的价格:" + productBoss.getPrice()); // 700(错误)
}
乐观锁测试(避免数据覆盖)
@Test
void testProductWithLock() {
// 1.小李获取商品价格
Product productLi = productMapper.selectById(1);
System.out.println("小李获取的价格:" + productLi.getPrice()); // 1000
// 2.小王获取商品价格
Product productWang = productMapper.selectById(1);
System.out.println("小王获取的价格:" + productWang.getPrice()); // 1000
// 3.小李加500并更新(成功,版本0→1)
productLi.setPrice(productLi.getPrice() + 500);
productMapper.updateById(productLi);
// 4.小王减300并更新,失败后重试
productWang.setPrice(productWang.getPrice() - 300);
int result = productMapper.updateById(productWang);
if (result == 0) {
// 更新失败,重新查询最新数据
Product productNew = productMapper.selectById(1);
// 基于最新数据更新
productNew.setPrice(productNew.getPrice() - 300);
productMapper.updateById(productNew); // 价格1500→1200
}
// 5.老板查询
Product productBoss = productMapper.selectById(1);
System.out.println("老板看到的价格:" + productBoss.getPrice()); // 1200(正确)
}
六、总结
核心要点
- 主键策略:雪花算法(ASSIGN_ID)是分布式系统首选,通过
@TableId局部配置或全局配置生效; - 条件构造器:QueryWrapper 链式调用实现复杂条件查询,覆盖所有单表查询场景,无需手写 SQL;
- 物理分页:配置分页拦截器后,通过 Page 对象一键实现分页,自动封装分页结果;
- 逻辑删除:
@TableLogic标识删除字段,将删除转为更新,防止数据误删且可恢复; - 乐观锁:
@Version标识版本号字段,解决读多写少场景的并发更新冲突,更新失败可重试。
进阶方向
- 代码生成器:自动生成实体类、Mapper、Service、Controller 代码;
- 通用 Service:继承
IService/ServiceImpl,封装更上层的业务逻辑; - 性能分析插件:监控 SQL 执行时间,定位性能瓶颈;
- 动态表名插件:实现多租户、分表等场景的动态表名切换。
更多推荐




所有评论(0)