MyBatis-Plus 进阶功能:分页插件与乐观锁的实战指南
在实际开发中,分页查询和并发数据修改是常见需求。MyBatis-Plus 提供了开箱即用的分页插件和乐观锁机制,无需手动编写复杂 SQL 即可实现这些功能。本文将详细讲解这两个功能的原理、配置步骤和实战案例,帮助你快速在项目中落地。
一、分页插件:轻松实现分页查询
传统分页需要手动编写LIMIT
语句并计算总页数,MyBatis-Plus 的分页插件通过拦截 SQL 自动添加分页条件,简化了这一过程。
1.1 分页插件的配置
使用分页插件只需两步:添加配置类并注册分页拦截器。
步骤 1:编写配置类
@Configuration
@MapperScan("com.qcby.mybatisplus.mapper") // 扫描Mapper接口所在包
public class MyBatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件,指定数据库类型(MySQL)interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}
说明:
MybatisPlusInterceptor
是 MyBatis-Plus 的核心拦截器,用于整合各种插件;PaginationInnerInterceptor
是分页插件,需指定数据库类型(如DbType.MYSQL
)以适配不同数据库的分页语法。
1.2 基础分页查询:使用 selectPage 方法
MyBatis-Plus 的BaseMapper
提供了selectPage
方法,直接传入Page
对象即可实现分页。
测试代码
@Test
public void testBasicPage() {// 创建Page对象,参数1:当前页(从1开始),参数2:每页条数Page<User> page = new Page<>(1, 5);// 调用selectPage,第二个参数为查询条件(null表示无额外条件)userMapper.selectPage(page, null);// 获取分页数据List<User> records = page.getRecords(); // 当前页数据列表long current = page.getCurrent(); // 当前页long size = page.getSize(); // 每页条数long total = page.getTotal(); // 总记录数long pages = page.getPages(); // 总页数boolean hasPrevious = page.hasPrevious(); // 是否有上一页boolean hasNext = page.hasNext(); // 是否有下一页// 打印结果System.out.println("当前页数据:" + records);System.out.println("当前页:" + current);System.out.println("每页条数:" + size);System.out.println("总记录数:" + total);System.out.println("总页数:" + pages);System.out.println("是否有上一页:" + hasPrevious);System.out.println("是否有下一页:" + hasNext);
}
生成的 SQL:
分页插件会自动在 SQL 后添加LIMIT
条件,并查询总记录数:
-- 查询当前页数据
SELECT id, username, age, email, is_deleted
FROM t_user
WHERE is_deleted=0
LIMIT 0,5-- 查询总记录数
SELECT COUNT(1)
FROM t_user
WHERE is_deleted=0
1.3 自定义 XML 分页:复杂查询的分页实现
对于复杂的自定义 SQL(如多表关联查询),可通过 XML 编写 SQL 并结合分页插件实现分页。
步骤 1:在 Mapper 接口定义方法
@Repository
public interface UserMapper extends BaseMapper<User> {/*** 根据年龄查询用户,分页返回* @param page 分页对象(必须放在参数第一位)* @param age 年龄条件* @return 分页结果*/IPage<User> selectPageByAge(@Param("page") Page<User> page, @Param("age") Integer age);
}
注意:分页对象Page
必须作为第一个参数,MyBatis-Plus 会自动从Page
中获取分页参数。
步骤 2:在 XML 中编写 SQL
<!-- resources/mapper/UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qcby.mybatisplus.mapper.UserMapper"><!-- 自定义分页查询:查询年龄大于指定值的用户 --><select id="selectPageByAge" resultType="com.qcby.mybatisplus.entity.User">SELECT id, username, age, email FROM t_user WHERE age > #{age}</select>
</mapper>
说明:无需手动添加LIMIT
,分页插件会自动处理。
步骤 3:配置 XML 路径(可选)
如果 XML 文件不在默认路径(resources/mapper/
),需在配置文件中指定:
mybatis-plus:mapper-locations: classpath:mybatis/mapper/*.xml # 自定义XML路径
步骤 4:测试自定义分页
@Test
public void testCustomPage() {// 创建分页对象(第1页,每页5条)Page<User> page = new Page<>(1, 5);// 调用自定义分页方法,查询年龄大于20的用户IPage<User> userPage = userMapper.selectPageByAge(page, 20);// 分页结果与基础分页一致,可通过IPage获取各种信息System.out.println("总记录数:" + userPage.getTotal());System.out.println("当前页数据:" + userPage.getRecords());
}
生成的 SQL:
-- 查询当前页数据(自动添加LIMIT)
SELECT id, username, age, email
FROM t_user
WHERE age > 20
LIMIT 0,5-- 查询总记录数
SELECT COUNT(1)
FROM t_user
WHERE age > 20
二、乐观锁:解决并发数据修改冲突
当多个用户同时修改同一条数据时,可能出现 "丢失更新" 问题(后提交的修改覆盖先提交的修改)。乐观锁通过版本号机制避免这一问题。
2.1 问题场景:并发修改导致的数据不一致
假设有一件商品初始价格为 100 元,小李和小王同时操作:
- 小李计划涨价 50 元(目标价格 150 元);
- 小王计划降价 30 元(目标价格 70 元)。
若无锁机制,最终价格可能变为 70 元(小王的修改覆盖了小李的),而非正确的 120 元(100+50-30)。
数据库增加商品表
CREATE TABLE t_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)
);
INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本 ', 100);
添加实体
@Data
public class Product {private Long id;private String name;private Integer price;private Integer version;
}
添加mapper
@Repository
public interface ProductMapper extends BaseMapper<Product> {
}
模拟冲突的代码
@Test
public void testConcurrentUpdate() {// 小李查询商品Product p1 = productMapper.selectById(1L);System.out.println("小李查询的价格:" + p1.getPrice()); // 100// 小王查询商品Product p2 = productMapper.selectById(1L);System.out.println("小王查询的价格:" + p2.getPrice()); // 100// 小李修改:100 + 50 = 150p1.setPrice(p1.getPrice() + 50);int result1 = productMapper.updateById(p1);System.out.println("小李修改结果:" + result1); // 1(成功)// 小王修改:100 - 30 = 70p2.setPrice(p2.getPrice() - 30);int result2 = productMapper.updateById(p2);System.out.println("小王修改结果:" + result2); // 1(成功)// 最终价格Product p3 = productMapper.selectById(1L);System.out.println("最终价格:" + p3.getPrice()); // 70(错误,应为120)
}
2.2 乐观锁的实现原理
乐观锁通过 "版本号" 机制实现,核心逻辑如下:
- 数据库表添加
version
字段(初始值 0); - 查询数据时,获取当前
version
; - 更新数据时,条件中携带查询时的
version
,并将version
自增 1; - 若
version
不匹配(已被其他用户修改),则更新失败。
示例 SQL:
- 查询时获取版本:
SELECT id, price, version FROM t_product WHERE id=1
(假设版本为 0); - 小李更新:
UPDATE t_product SET price=150, version=1 WHERE id=1 AND version=0
(成功,版本变为 1); - 小王更新:
UPDATE t_product SET price=70, version=1 WHERE id=1 AND version=0
(失败,因版本已变为 1)。
2.3 乐观锁的实现步骤
步骤 1:数据库添加 version 字段
-- 商品表结构
CREATE TABLE t_product (id BIGINT PRIMARY KEY,name VARCHAR(30) COMMENT '商品名称',price INT COMMENT '价格',version INT DEFAULT 0 COMMENT '乐观锁版本号' -- 新增版本字段
);-- 初始化数据
INSERT INTO t_product (id, name, price) VALUES (1, '外星人笔记本', 100);
步骤 2:实体类添加 @Version 注解
@Data
public class Product {private Long id;private String name;private Integer price;@Version // 标识该字段为乐观锁版本号private Integer version;
}
步骤 3:配置乐观锁插件
在之前的配置类中添加乐观锁拦截器:
@Configuration
@MapperScan("com.qcby.mybatisplus.mapper")
public class MyBatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 乐观锁插件interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
}
2.4 测试乐观锁效果
重新运行并发修改测试:
@Test
public void testOptimisticLock() {// 小李查询商品(version=0)Product p1 = productMapper.selectById(1L);// 小王查询商品(version=0)Product p2 = productMapper.selectById(1L);// 小李修改:price=150,version=0→1p1.setPrice(p1.getPrice() + 50);int result1 = productMapper.updateById(p1); System.out.println("小李修改结果:" + result1); // 1(成功)// 小王修改:条件version=0,但实际已变为1,修改失败p2.setPrice(p2.getPrice() - 30);int result2 = productMapper.updateById(p2); System.out.println("小王修改结果:" + result2); // 0(失败)// 小王重试:重新查询最新数据(version=1)if (result2 == 0) {p2 = productMapper.selectById(1L); // 此时price=150,version=1p2.setPrice(p2.getPrice() - 30); // 150-30=120result2 = productMapper.updateById(p2); // 更新条件version=1→2System.out.println("小王重试结果:" + result2); // 1(成功)}// 最终价格Product p3 = productMapper.selectById(1L);System.out.println("最终价格:" + p3.getPrice()); // 120(正确)
}
生成的 SQL:
- 小李的更新:
UPDATE t_product SET name=?, price=?, version=1 WHERE id=1 AND version=0
(成功); - 小王首次更新:
UPDATE t_product SET name=?, price=?, version=1 WHERE id=1 AND version=0
(失败,version 不匹配); - 小王重试更新:
UPDATE t_product SET name=?, price=?, version=2 WHERE id=1 AND version=1
(成功)。
2.5 乐观锁的适用场景
- 适合读多写少的场景(如商品详情页频繁查询,偶尔修改价格);
- 不适合写冲突频繁的场景(此时悲观锁更合适);
- 核心是 "乐观":认为冲突概率低,通过版本号检测冲突,而非提前加锁阻塞。