MyBatis-Plus常用插件
# MyBatis-Plus常用插件
# 1. 分页插件:轻松实现高效分页
MyBatis-Plus提供的分页插件使开发者能够轻松实现数据库分页查询,无需手写复杂的SQL语句。本章将详细讲解分页插件的配置、使用及优化技巧。
# 1.1 配置分页插件
要使用分页功能,首先需要在项目中配置分页插件。MyBatis-Plus 3.4.0及以上版本使用拦截器的方式配置插件。
/**
* MyBatis-Plus插件配置类
* 用于注册分页插件等MyBatis-Plus提供的功能增强插件
*/
@Configuration
@MapperScan("com.example.project.mapper") // 指定Mapper接口的扫描路径
public class MyBatisPlusConfig {
/**
* 配置MyBatis-Plus插件拦截器
* 在此拦截器中可以添加多个内部插件,如分页插件、乐观锁插件等
* @return 配置好的拦截器实例
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建MyBatis-Plus拦截器实例
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,并指定数据库类型为MySQL
// 支持的数据库类型包括:MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、PostgreSQL等
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1.2 分页查询基础用法
分页查询的核心是Page<T>
对象,它封装了分页的核心参数和方法。下面是分页查询的基本用法:
/**
* 基础分页查询示例
* 演示如何使用MyBatis-Plus分页插件进行简单的分页查询
*/
@Test
public void testBasicPage() {
// 创建分页对象,指定当前页码和每页显示的记录数
// 参数1:current - 当前页码,从1开始计数
// 参数2:size - 每页显示的记录数
Page<User> page = new Page<>(1, 5);
// 构建查询条件(可选)
// 如不需要条件,可以传入null
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(User::getAge, 18); // 查询年龄大于等于18的用户
// 执行分页查询
// selectPage方法会自动处理分页逻辑,无需手动编写LIMIT语句
// 查询结果会被封装到page对象中
Page<User> userPage = userMapper.selectPage(page, queryWrapper);
// 获取分页信息
System.out.println("总记录数: " + userPage.getTotal()); // 符合条件的总记录数
System.out.println("当前页码: " + userPage.getCurrent()); // 当前页码
System.out.println("每页记录数: " + userPage.getSize()); // 每页显示的记录数
System.out.println("总页数: " + userPage.getPages()); // 总页数
System.out.println("是否有上一页: " + userPage.hasPrevious()); // 是否有上一页
System.out.println("是否有下一页: " + userPage.hasNext()); // 是否有下一页
// 获取分页数据列表
List<User> users = userPage.getRecords(); // 当前页的记录列表
users.forEach(System.out::println);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 1.3 分页插件高级特性
MyBatis-Plus的分页插件除了基本的分页功能外,还提供了许多高级特性。
# 1.3.1 排序功能
可以在创建Page
对象时指定排序规则:
/**
* 分页排序示例
* 演示如何在分页查询中添加排序条件
*/
@Test
public void testPageWithSort() {
// 方式一:通过Page对象设置排序
// 创建分页对象并设置排序规则
Page<User> page = new Page<>(1, 10);
// 按年龄降序,再按ID升序排列
page.addOrder(OrderItem.desc("age"));
page.addOrder(OrderItem.asc("id"));
// 执行分页查询
Page<User> userPage1 = userMapper.selectPage(page, null);
// 方式二:通过条件构造器设置排序
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(User::getAge)
.orderByAsc(User::getId);
Page<User> userPage2 = userMapper.selectPage(new Page<>(1, 10), queryWrapper);
// 获取查询结果
userPage2.getRecords().forEach(System.out::println);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 1.3.2 只查询总记录数
有时我们只需要知道符合条件的记录总数,而不需要查询具体数据:
/**
* 只查询总记录数示例
*/
@Test
public void testCountOnly() {
// 创建分页对象,设置只查询总记录数
Page<User> page = new Page<>(1, 10);
page.setSearchCount(true); // 设置查询总记录数(默认为true)
page.setMaxLimit(100L); // 设置最大单页限制数量
// 查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(User::getName, "张");
// 执行查询
userMapper.selectPage(page, queryWrapper);
// 获取总记录数
long total = page.getTotal();
System.out.println("符合条件的记录总数: " + total);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1.3.3 溢出总页数处理
当请求的页码大于最大页码时,可以设置返回首页或最后一页:
/**
* 页码溢出处理示例
*/
@Test
public void testPageOverflow() {
// 假设总共只有3页数据,但请求第10页
// 创建分页对象,并设置页码溢出后返回首页
Page<User> page1 = new Page<>(10, 10, true);
// true表示查询总记录数
// 设置页码溢出后的处理策略
PaginationInnerInterceptor interceptor = new PaginationInnerInterceptor();
// 溢出总页数后是否进行处理
interceptor.setOverflow(true);
// 溢出后返回首页
interceptor.setMaxLimit(500L); // 单页最大条数限制
// 执行查询(此时会返回第1页的数据,而不是空结果)
Page<User> result = userMapper.selectPage(page1, null);
System.out.println("当前页码: " + result.getCurrent());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2. 自定义分页查询:满足复杂业务需求
虽然MyBatis-Plus内置的分页功能已经很强大,但在复杂业务场景中,我们可能需要自定义分页查询来满足特定需求。
# 2.1 自定义Mapper方法
在Mapper接口中定义自定义分页方法,第一个参数必须是Page<T>
:
/**
* 用户Mapper接口
*/
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页查询方法
* @param page 分页对象,必须是第一个参数
* @param age 年龄条件
* @param name 姓名条件(模糊匹配)
* @return 分页结果,包含查询到的记录和分页信息
*/
Page<UserVO> selectUserWithRolePage(
@Param("page") Page<UserVO> page,
@Param("age") Integer age,
@Param("name") String name
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.2 编写XML映射文件
为自定义分页方法编写对应的XML语句:
<!-- UserMapper.xml -->
<mapper namespace="com.example.project.mapper.UserMapper">
<!-- 自定义分页查询 -->
<!--
注意:使用MyBatis-Plus分页插件时,无需手动添加LIMIT语句
分页插件会自动识别Page参数并添加分页语句
-->
<select id="selectUserWithRolePage" resultType="com.example.project.vo.UserVO">
SELECT
u.id, u.username, u.age, u.email,
r.role_name
FROM t_user u
LEFT JOIN t_user_role ur ON u.id = ur.user_id
LEFT JOIN t_role r ON ur.role_id = r.id
<where>
<!-- 动态条件:如果提供了年龄参数,则添加年龄条件 -->
<if test="age != null">
AND u.age > #{age}
</if>
<!-- 动态条件:如果提供了姓名参数,则添加姓名模糊查询条件 -->
<if test="name != null and name != ''">
AND u.username LIKE CONCAT('%', #{name}, '%')
</if>
</where>
ORDER BY u.id DESC
</select>
</mapper>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2.3 调用自定义分页查询
在Service层或测试代码中调用自定义分页方法:
/**
* 自定义分页查询示例
* 演示如何使用自定义的分页查询方法
*/
@Test
public void testCustomPage() {
// 创建分页对象
Page<UserVO> page = new Page<>(1, 5);
// 设置查询参数
Integer minAge = 20; // 最小年龄
String nameKeyword = "张"; // 姓名关键字
// 调用自定义分页方法
Page<UserVO> result = userMapper.selectUserWithRolePage(page, minAge, nameKeyword);
// 处理分页结果
System.out.println("总记录数: " + result.getTotal());
System.out.println("总页数: " + result.getPages());
// 遍历查询结果
List<UserVO> userVOList = result.getRecords();
userVOList.forEach(userVO -> {
System.out.println("用户ID: " + userVO.getId());
System.out.println("用户名: " + userVO.getUsername());
System.out.println("角色名: " + userVO.getRoleName());
System.out.println("--------------------");
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 2.4 自定义分页的关键点
- 第一个参数必须是Page对象:这是MyBatis-Plus识别分页的关键。
- 无需手动添加LIMIT语句:分页插件会自动处理。
- 可以结合动态SQL:实现条件查询和复杂关联。
- 返回值也应为Page对象:保持一致的分页操作体验。
# 3. 乐观锁插件:并发数据安全保障
乐观锁是一种并发控制方法,它假设多用户并发的事务在处理时不会彼此互相影响,因此不会加锁。只在提交操作时检查是否违反数据完整性。
# 3.1 乐观锁的原理
乐观锁的核心原理是:
- 取出记录时,获取当前版本号(version字段值)
- 更新时,带上这个版本号
- 执行更新语句时,WHERE条件中加入版本号的判断
- 如果数据已被其他线程修改,版本号不匹配,更新失败
# 3.2 乐观锁的应用场景
乐观锁适用于以下场景:
- 读多写少:大部分时间都在读取数据,偶尔才会修改的场景
- 并发冲突较少:操作冲突概率较低的场景
- 不允许数据被错误覆盖:必须确保数据准确性的业务
以商品价格修改为例,展示乐观锁的实际应用:
# 3.3 乐观锁插件的配置
首先,需要在MyBatis-Plus配置中添加乐观锁插件:
/**
* MyBatis-Plus插件配置类
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 配置MyBatis-Plus插件
* 注册分页插件和乐观锁插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 添加乐观锁插件
// 该插件会自动在更新操作时添加版本号条件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.4 实体类中的版本字段配置
在实体类中,需要使用@Version
注解标记版本号字段:
/**
* 商品实体类
* 演示乐观锁的使用
*/
@Data
@TableName("t_product")
public class Product {
/**
* 商品ID
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 商品名称
*/
private String name;
/**
* 商品价格
*/
private Integer price;
/**
* 版本号,用于乐观锁控制
* 使用@Version注解标记该字段为乐观锁版本号字段
* 每次更新操作时,该字段会自动+1
*/
@Version
private Integer version;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 3.5 乐观锁的实际应用示例
下面通过一个完整的例子来说明乐观锁的工作原理:
# 3.5.1 准备数据库表
-- 创建商品表,包含版本号字段
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, version) VALUES (1, '外星人笔记本', 100, 0);
2
3
4
5
6
7
8
9
10
11
# 3.5.2 模拟并发修改场景
/**
* 乐观锁并发修改测试
* 模拟两个用户同时修改同一商品价格的场景
*/
@Test
public void testOptimisticLock() {
// 场景:两个用户同时获取商品信息,然后分别进行修改
// 1. 小李查询商品信息
Product productLi = productMapper.selectById(1L);
System.out.println("小李查询到的商品价格:" + productLi.getPrice());
// 2. 小王也查询商品信息
Product productWang = productMapper.selectById(1L);
System.out.println("小王查询到的商品价格:" + productWang.getPrice());
// 3. 小李将价格+50
productLi.setPrice(productLi.getPrice() + 50);
// 更新操作,此时version会自动+1
int resultLi = productMapper.updateById(productLi);
System.out.println("小李修改结果:" + (resultLi > 0 ? "成功" : "失败"));
// 4. 小王将价格-30
productWang.setPrice(productWang.getPrice() - 30);
// 更新操作,但由于此时数据库中version已经被小李的操作更新,所以此次更新会失败
int resultWang = productMapper.updateById(productWang);
System.out.println("小王修改结果:" + (resultWang > 0 ? "成功" : "失败"));
// 5. 老板查询最终的商品价格
Product productFinal = productMapper.selectById(1L);
System.out.println("最终的商品价格:" + productFinal.getPrice());
System.out.println("当前的版本号:" + productFinal.getVersion());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 3.5.3 更新失败时的重试机制
在实际应用中,当乐观锁导致更新失败时,通常需要实现重试机制:
/**
* 乐观锁并发修改测试(含重试机制)
*/
@Test
public void testOptimisticLockWithRetry() {
// 1. 小李查询商品信息
Product productLi = productMapper.selectById(1L);
System.out.println("小李查询到的商品价格:" + productLi.getPrice());
// 2. 小王也查询商品信息
Product productWang = productMapper.selectById(1L);
System.out.println("小王查询到的商品价格:" + productWang.getPrice());
// 3. 小李将价格+50
productLi.setPrice(productLi.getPrice() + 50);
int resultLi = productMapper.updateById(productLi);
System.out.println("小李修改结果:" + (resultLi > 0 ? "成功" : "失败"));
// 4. 小王将价格-30
productWang.setPrice(productWang.getPrice() - 30);
int resultWang = productMapper.updateById(productWang);
// 5. 如果小王修改失败,实现重试逻辑
if (resultWang == 0) {
System.out.println("小王修改失败,开始重试...");
// 重新查询商品信息(获取最新版本)
Product productWangRetry = productMapper.selectById(1L);
System.out.println("重试查询到的商品价格:" + productWangRetry.getPrice());
// 在最新价格基础上减去30
productWangRetry.setPrice(productWangRetry.getPrice() - 30);
// 再次尝试更新
int retryResult = productMapper.updateById(productWangRetry);
System.out.println("小王重试修改结果:" + (retryResult > 0 ? "成功" : "失败"));
}
// 6. 老板查询最终的商品价格
Product productFinal = productMapper.selectById(1L);
System.out.println("最终的商品价格:" + productFinal.getPrice());
System.out.println("当前的版本号:" + productFinal.getVersion());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 3.6 乐观锁的实现原理分析
当我们使用乐观锁插件时,MyBatis-Plus会在执行更新操作时自动添加版本号条件:
-- 原始更新SQL(不带乐观锁)
UPDATE t_product SET name = ?, price = ? WHERE id = ?;
-- 添加乐观锁后的SQL
UPDATE t_product
SET name = ?, price = ?, version = version + 1
WHERE id = ? AND version = ?;
2
3
4
5
6
7
关键点:
- 更新时会检查当前版本号是否与查询时的版本号一致
- 如果一致,则更新成功并将版本号+1
- 如果不一致,则说明数据已被其他线程修改,更新失败(返回影响行数为0)
# 3.7 乐观锁使用注意事项
- 仅支持updateById和update(entity, wrapper)方法:其他更新方法不支持乐观锁。
- 必须携带版本号:更新操作必须带上版本号字段。
- 支持的数据类型:version字段支持int、long、date、timestamp等类型。
- 使用场景:适用于读多写少,并发冲突概率低的场景。
- 需要处理更新失败:业务代码中需要判断更新结果,必要时实现重试逻辑。
# 4. 插件组合使用
MyBatis-Plus的插件可以组合使用,实现更丰富的功能。以下是一些最佳实践:
# 4.1 分页+条件查询+排序
/**
* 综合查询示例
* 结合分页、条件查询和排序
*/
@Test
public void testCombinedQuery() {
// 创建分页对象
Page<User> page = new Page<>(1, 10);
// 创建条件构造器
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "张")
.between(User::getAge, 20, 30)
.orderByDesc(User::getCreateTime);
// 执行查询
Page<User> result = userMapper.selectPage(page, wrapper);
// 处理结果
result.getRecords().forEach(System.out::println);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 4.2 分页+乐观锁+自定义查询
/**
* 自定义SQL分页查询,并使用乐观锁更新
*/
@Test
public void testCustomPageWithLock() {
// 1. 执行自定义分页查询
Page<ProductVO> page = new Page<>(1, 5);
Page<ProductVO> result = productMapper.selectProductWithCategoryPage(page);
// 2. 获取第一个商品进行修改
if (!result.getRecords().isEmpty()) {
ProductVO productVO = result.getRecords().get(0);
// 3. 转换为实体对象并修改价格
Product product = new Product();
product.setId(productVO.getId());
product.setPrice(productVO.getPrice() + 100);
product.setVersion(productVO.getVersion()); // 设置版本号
// 4. 使用乐观锁更新
int updateResult = productMapper.updateById(product);
System.out.println("更新结果: " + (updateResult > 0 ? "成功" : "失败"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 5. 常见问题与解决方案
# 5.1 分页插件问题
# 5.1.1 分页结果不准确
问题描述:查询结果的总记录数不符合预期。
解决方案:
- 检查是否正确配置了分页插件
- 确认数据库类型是否正确
- 避免在分页方法前使用
SELECT COUNT(*)
// 错误的做法
int count = userMapper.selectCount(wrapper); // 不要单独查询总数
Page<User> page = userMapper.selectPage(new Page<>(1, 10), wrapper);
// 正确的做法
Page<User> page = userMapper.selectPage(new Page<>(1, 10), wrapper);
long count = page.getTotal(); // 直接从分页结果获取总数
2
3
4
5
6
7
# 5.1.2 多表关联查询分页问题
问题描述:关联多个表的查询结果分页不正确。
解决方案:使用自定义分页查询,确保SQL语句正确使用JOIN。
# 5.2 乐观锁问题
# 5.2.1 更新一直失败
问题描述:使用乐观锁时,更新操作总是失败。
解决方案:
- 确保查询和更新之间没有其他线程修改数据
- 检查实体类中的版本号字段是否正确设置了
@Version
注解 - 实现重试机制
# 5.2.2 部分字段更新问题
问题描述:只更新部分字段时,乐观锁不生效。
解决方案:使用updateById
方法或确保update
方法中的实体对象包含版本号字段。
// 正确的部分字段更新方式
LambdaUpdateWrapper<Product> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Product::getId, 1L)
.set(Product::getPrice, 150);
// 创建带版本号的实体对象
Product product = new Product();
product.setVersion(currentVersion); // 必须设置当前版本号
// 执行更新
productMapper.update(product, updateWrapper);
2
3
4
5
6
7
8
9
10
11