MyBatis-Plus 入门教程之六

前言

版本说明

本文的教程内容是基于 MyBatis-Plus 3.5.2 版本讲解的,若你使用的是 2.x 或其他版本,可能会有部分知识点和案例代码不兼容。

MyBatis-Plus 插件介绍

InnerInterceptor 接口

MyBatis-Plus 的插件都是基于 InnerInterceptor 接口来实现的,目前已有的插件:

  • 分页 - PaginationInnerInterceptor
  • 多租户 - TenantLineInnerInterceptor
  • 动态表名 - DynamicTableNameInnerInterceptor
  • 乐观锁 - OptimisticLockerInnerInterceptor
  • SQL 性能规范 - IllegalSQLInnerInterceptor
  • 防止全表更新与删除 - BlockAttackInnerInterceptor

使用多个插件时需要注意顺序关系,建议使用如下顺序

  • 多租户、动态表名
  • 分页、乐观锁
  • SQL 性能规范、防止全表更新与删除
  • 总结:对 SQL 进行单次改造的优先放入,不对 SQL 进行改造的最后放入

忽略插件拦截的注解

@InterceptorIgnore 注解可用于忽略插件的拦截,支持注解在 Mapper 上以及 Mapper 方法上,若两者同时存在则 Mapper 方法 比 Mapper 的优先级高。各属性表示对应的插件,支持取值为 truefalse10onoff。属性值返回 true 表示不走插件(在配置了插件的情况下,不填则默认表示 false)。属性列表如下:

属性名类型默认值描述
tenantLineString“” 行级租户
dynamicTableNameString“” 动态表名
blockAttackString“” 攻击 SQL 阻断解析器,防止全表更新与删除
illegalSqlString“” 垃圾 SQL 拦截

更多的使用说明详见 @InterceptorIgnore 注解的源码注释

MyBatis-Plus 插件使用

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-07

MyBatis-Plus 分页插件

分页插件的介绍

PaginationInnerInterceptor 分页拦截器类的属性说明如下:

属性名类型默认值描述
overflowbooleanfalse 溢出总页数后是否进行处理 (默认不处理,参见 插件#continuePage 方法)
maxLimitLong 单页分页条数限制 (默认无限制,参见 插件#handlerLimit 方法)
dbTypeDbType 数据库类型 (根据类型获取应使用的分页方言,参见 插件#findIDialect 方法)
dialectIDialect 方言实现类 (参见 插件#findIDialect 方法)

注意

  • 建议单一数据库类型的均设置 dbType 属性,避免每次分页都去抓取数据库类型
  • 生成 countSql 会在 left join 的表不参与 where 条件的情况下,把 left join 优化掉
  • 所以建议任何带有 left join 的 SQL,都写成标准 SQL,即给于表一个别名,字段也要 别名.字段

分页插件的使用

分页插件的配置步骤

只需要使用 Spring XML 方式、 SpringBoot 配置类方式或者 MyBatis 配置文件方式中的任意一种配置乐观锁插件即可。

配置分页插件

注入 MybatisPlusInterceptor 类,并配置 PaginationInnerInterceptor 拦截器。

Spring XML 方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="plugins">
<array>
<ref bean="mybatisPlusInterceptor"/>
</array>
</property>
</bean>

<bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors">
<list>
<ref bean="paginationInnerInterceptor"/>
</list>
</property>
</bean>

<bean id="paginationInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor">
<constructor-arg name="dbType" value="MYSQL"/>
</bean>
SpringBoot 配置类方式
  • 配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* MyBatis-Plus 配置类
*/
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MybatisPlusConfig {

/**
* 添加 MyBatis-Plus 分页插件
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

}

若 MyBatis-Plus 使用的是较低的版本(例如 3.3.1),则配置类的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* MyBatis-Plus 配置类
*/
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MyBatisPlusConfiguration {

/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL);
return paginationInterceptor;
}

}
  • 启动类

提示

特殊情况下,可能还需要在启动类上通过 @ComponentScan 注解来扫描上面定义的 MybatisPlusConfig 配置类。

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@ComponentScan("com.clay.mybatis")
public class MyBatisPlusApplication {

public static void main(String[] args) {
SpringApplication.run(MyBatisPlusApplication.class, args);
}

}
MyBatis 配置文件方式
1
2
3
4
5
6
7
8
9
<configuration>
<plugins>
<!-- property 的配置说明详见 MybatisPlusInterceptor.setProperties() 的源码方法注释 -->
<plugin interceptor="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="@page" value="com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"/>
<property name="page:dbType" value="mysql"/>
</plugin>
</plugins>
</configuration>
分页操作(基于 AR 模式)

MyBatis-Plus 分页插件支持在 ActiveRecord 模式下使用,示例代码如下。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Employee extends Model<Employee> {

private Long id;
private String lastName;
private String gender;
private String email;
private Integer age;

....

@Override
public Serializable pkVal() {
return this.id;
}

}
  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
1
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
@SpringBootTest
public class PagePluginTest {

/**
* 基于 ActiveRecord 模式的分页查询
*/
@Test
public void selectByPageForAR() {
// 分页信息
Page<Employee> page = new Page<>(2, 2);

// 查询条件
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("gender", "1");

// 执行分页查询
Employee employee = new Employee();
employee.selectPage(page, wrapper);

// 获取分页结果
List<Employee> list = page.getRecords();
list.forEach(System.out::println);

// 获取分页信息
System.out.println("总页数: " + page.getPages());
System.out.println("总记录数: " + page.getTotal());
System.out.println("当前的页码: " + page.getCurrent());
System.out.println("每页的记录数: " + page.getSize());
System.out.println("是否有上一页: " + page.hasPrevious());
System.out.println("是否有下一页: " + page.hasNext());
}

}

执行上面的测试代码后,控制台输出的日志信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
==>  Preparing: SELECT COUNT(*) AS total FROM t_employee WHERE (gender = ?)
==> Parameters: 1(String)
<== Columns: total
<== Row: 4
<== Total: 1

==> Preparing: SELECT id,email,last_name,gender,age FROM t_employee WHERE (gender = ?) LIMIT ?,?
==> Parameters: 1(String), 2(Long), 2(Long)
<== Columns: id, email, last_name, gender, age
<== Row: 3, jim@gmail.com, Jim, 1, 26
<== Row: 4, peter@gmail.com, Peter, 1, 29
<== Total: 2

Employee [id=3, lastName=Jim, gender=1, email=jim@gmail.com, age=26]
Employee [id=4, lastName=Peter, gender=1, email=peter@gmail.com, age=29]
总页数: 2
总记录数: 4
当前的页码: 2
每页的记录数: 2
是否有上一页: true
是否有下一页: false
分页操作(基于通用 Mapper)

MyBatis-Plus 分页插件支持通用 Mapper 的使用,示例代码如下。

  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
1
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
@SpringBootTest
public class PagePluginTest {

@Autowired
private EmployeeMapper empMapper;

/**
* 基于通用 Mapper 的分页查询
*/
@Test
public void selectByPage() {
// 分页信息
Page<Employee> page = new Page<>(2, 2);

// 查询条件
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("gender", "1");

// 执行分页查询
empMapper.selectPage(page, wrapper);

// 获取分页结果
List<Employee> list = page.getRecords();
list.forEach(System.out::println);

// 获取分页信息
System.out.println("总页数: " + page.getPages());
System.out.println("总记录数: " + page.getTotal());
System.out.println("当前的页码: " + page.getCurrent());
System.out.println("每页的记录数: " + page.getSize());
System.out.println("是否有上一页: " + page.hasPrevious());
System.out.println("是否有下一页: " + page.hasNext());
}

}
分页操作(基于自定义 Mapper 方法)

MyBatis-Plus 分页插件支持自定义 Mapper 方法的分页查询,示例代码如下。

  • Mapper 接口

提示

1、自定义 Mapper 方法时,需要指定返回值的类型为 IPageList,同时还需要指定其中的一个方法参数的类型为 IPage
2、若自定义 Mapper 方法的返回类型是 IPage,则入参的 IPage 不能为 null,因为返回的 IPage 等于入参的 IPage;如果想临时不分页,可以在初始化 IPage 时指定 size 属性为小于零的值即可
3、若自定义 Mapper 方法的返回类型是 List,则入参的 IPage 可以为 null(为 null 则表示不分页)
4、若在 SQL 映射文件中,需要从 page 对象里取值,可以使用 page.属性名 的方式来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface EmployeeMapper extends BaseMapper<Employee> {

/**
* 第一种方式:方法的返回值为 IPage 类型
*
* @param page
* @param gender
* @return
*/
IPage<Employee> queryByPage(IPage<Employee> page, @Param("gender") String gender);

/**
* 第二种方式:方法的返回值为 List 类型
*
* @param page
* @param gender
* @return
*/
List<Employee> queryListByPage(IPage<Employee> page, @Param("gender") String gender);

}
  • SQL 映射文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="queryByPage" resultType="Employee">
select id, email, last_name as lastName, gender, age
from t_employee
where gender = #{gender}
</select>

<select id="queryListByPage" resultType="Employee">
select id, email, last_name as lastName, gender, age
from t_employee
where gender = #{gender}
</select>

</mapper>
  • Junit 测试代码
1
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
44
45
46
47
48
49
50
51
52
@SpringBootTest
public class PagePluginTest {

@Autowired
private EmployeeMapper empMapper;

/**
* 基于自定义 Mapper 方法(返回值为 IPage)的分页查询
*/
@Test
public void queryByPage() {
// 分页信息
Page<Employee> page = new Page<>(2, 2);

// 执行分页查询
empMapper.queryByPage(page, "1");

// 获取分页结果
List<Employee> list = page.getRecords();
list.forEach(System.out::println);

// 获取分页信息
System.out.println("总页数: " + page.getPages());
System.out.println("总记录数: " + page.getTotal());
System.out.println("当前的页码: " + page.getCurrent());
System.out.println("每页的记录数: " + page.getSize());
System.out.println("是否有上一页: " + page.hasPrevious());
System.out.println("是否有下一页: " + page.hasNext());
}

/**
* 基于自定义 Mapper 方法(返回值为 List)的分页查询
*/
@Test
public void queryListByPage() {
// 分页信息
Page<Employee> page = new Page<>(2, 2);

// 执行分页查询
List<Employee> list = empMapper.queryListByPage(page, "1");
list.forEach(System.out::println);

// 获取分页信息
System.out.println("总页数: " + page.getPages());
System.out.println("总记录数: " + page.getTotal());
System.out.println("当前的页码: " + page.getCurrent());
System.out.println("每页的记录数: " + page.getSize());
System.out.println("是否有上一页: " + page.hasPrevious());
System.out.println("是否有下一页: " + page.hasNext());
}

}

MyBatis-Plus 乐观锁插件

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-08

真实业务场景

一件商品,成本价是 80 元,售价是 100 元。老板先是通知小李,说你去把商品价格增加 50 元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到 150 元,价格太高,可能会影响销量。又通知小王,你把商品价格降低 30 元。此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格 100 元;小王也在操作,取出的商品价格也是 100 元。小李将价格加了 50 元,并将 100 + 50 = 150 元存入了数据库;小王将商品减了 30 元,并将 100 - 30 = 70 元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。现在商品价格是 70 元,比成本价低 10 元。几分钟后,这个商品很快出售了 1 千多件商品,老板亏了 1 万多元。如果这里使用乐观锁,在小王保存价格前,可以检查到价格是否被人修改过。如果价格被修改过,则重新取出被修改后的价格(150 元),这样他最终会将 120 元存入数据库。如果是使用悲观锁,在小李取出数据时,小王只能等小李完成价格的更改之后,才能对价格进行操作,这也会保证最终的价格是 120 元。

模拟数据更改冲突

  • 创建数据库表
1
2
3
4
5
6
7
8
9
10
11
12
-- 创建数据库表
CREATE TABLE `t_product`
(
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
name VARCHAR(50) DEFAULT NULL COMMENT '商品名称',
price DECIMAL(10, 5) DEFAULT 0 COMMENT '价格',
version BIGINT(20) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入表数据
insert into t_product (id, name, price) values (1, 'C++ Primer Plus', 100);
  • 实体类
1
2
3
4
5
6
7
8
9
10
public class Product {

private Long id;
private String name;
private Long version;
private BigDecimal price;

...

}
  • Mapper 接口
1
2
3
public interface ProductMapper extends BaseMapper<Product> {

}
  • Junit 测试代码
1
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
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private ProductMapper productMapper;

@Test
public void updatePrice() {

Product product1 = productMapper.selectById(1L);
System.out.println("小李取出的商品价格:" + product1.getPrice());

Product product2 = productMapper.selectById(1L);
System.out.println("小王取出的商品价格:" + product2.getPrice());

product1.setPrice(product1.getPrice().add(new BigDecimal(50)));
int result1 = productMapper.updateById(product1);
System.out.println("小李修改商品价格的结果: " + (result1 > 0));

product2.setPrice(product2.getPrice().subtract(new BigDecimal(30)));
int result2 = productMapper.updateById(product2);
System.out.println("小王修改商品价格的结果: " + (result2 > 0));

Product product3 = productMapper.selectById(1L);
System.out.println("最终的商品价格: " + product3.getPrice());
}

}

执行上面的测试代码后,控制台输出的日志信息如下,观察后可发现最终的商品价格并不是预期的 120 元。

1
2
3
4
5
小李取出的商品价格:100.00000
小王取出的商品价格:100.00000
小李修改商品价格的结果: true
小王修改商品价格的结果: true
最终的商品价格: 70.00000

乐观锁的实现流程

  • 第一步:在数据库表中添加 version 字段
1
2
3
4
5
6
7
8
CREATE TABLE `t_product`
(
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
name VARCHAR(50) DEFAULT NULL COMMENT '商品名称',
price DECIMAL(10, 5) DEFAULT 0 COMMENT '价格',
version BIGINT(20) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 第二步:读取数据库表记录时,获取当前记录的 version(oldVersion)
1
select id, name, price, version from t_product where id = 1;
  • 第三步:更新数据库表记录时,执行 version + 1,如果 where 语句中的 version 版本不满足条件,则更新失败
1
update t_product set price = price + 50, version = version + 1 where id = 1 and version = oldVersion;

或者可以简单理解为以下的 SQL 语句

1
update t_product set price = newPrice, version = newVersion where id = 1 and version = oldVersion;

乐观锁插件的使用

乐观锁插件的配置步骤

  • 第一步:使用 Spring XML 方式、 SpringBoot 配置类方式或者 MyBatis 配置文件方式中的任意一种配置乐观锁插件
  • 第二步:在实体类的属性上添加 @Version 注解
配置乐观锁插件

注入 MybatisPlusInterceptor 类,并配置 OptimisticLockerInnerInterceptor 拦截器。

Spring XML 方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="plugins">
<array>
<ref bean="mybatisPlusInterceptor"/>
</array>
</property>
</bean>

<bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors">
<list>
<ref bean="optimisticLockerInnerInterceptor"/>
</list>
</property>
</bean>

<bean id="optimisticLockerInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor"/>
SpringBoot 配置类方式
  • 配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* MyBatis-Plus 配置类
*/
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MybatisPlusConfig {

/**
* 添加 MyBatis-Plus 乐观锁插件
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}

}
  • 启动类

提示

特殊情况下,可能还需要在启动类上通过 @ComponentScan 注解来扫描上面定义的 MybatisPlusConfig 配置类。

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@ComponentScan("com.clay.mybatis")
public class MyBatisPlusApplication {

public static void main(String[] args) {
SpringApplication.run(MyBatisPlusApplication.class, args);
}

}
MyBatis 配置文件方式
1
2
3
4
5
6
7
8
<configuration>
<plugins>
<!-- property 的配置说明详见 MybatisPlusInterceptor.setProperties() 的源码方法注释 -->
<plugin interceptor="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="@optimisticLocker" value="com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor"/>
</plugin>
</plugins>
</configuration>
添加 @Version 注解

在实体类的属性上添加 @Version 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Product {

private Long id;

private String name;

@Version
private Long version;

private BigDecimal price;

...

}

特别注意

  • @Version 注解支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整数类型下 newVersion = oldVersion + 1,其中的 newVersion 会回写到 Entity 中
  • @Version 注解仅支持 updateById(id)update(entity, wrapper) 方法
  • 在使用 update(entity, wrapper) 方法的情况下,wrapper 不能复用!
Junit 单元测试代码
  • Mapper 接口
1
2
3
public interface ProductMapper extends BaseMapper<Product> {

}
  • Junit 测试代码
1
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
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private ProductMapper productMapper;

@Test
public void updatePrice() {

Product product1 = productMapper.selectById(1L);
System.out.println("小李取出的商品价格:" + product1.getPrice());

Product product2 = productMapper.selectById(1L);
System.out.println("小王取出的商品价格:" + product2.getPrice());

product1.setPrice(product1.getPrice().add(new BigDecimal(50)));
int result1 = productMapper.updateById(product1);
System.out.println("小李修改商品价格的结果: " + (result1 > 0));

product2.setPrice(product2.getPrice().subtract(new BigDecimal(30)));
int result2 = productMapper.updateById(product2);
System.out.println("小王修改商品价格的结果: " + (result2 > 0));

Product product3 = productMapper.selectById(1L);
System.out.println("最终的商品价格: " + product3.getPrice());
}

}

执行上面的测试代码后,控制台输出的日志信息如下,观察后可发现最终只有小李成功更改了商品价格,也就是商品价格的更改不会再发生冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
==>  Preparing: SELECT id,name,price,version FROM t_product WHERE id=?
==> Parameters: 1(Long)
<== Columns: id, name, price, version
<== Row: 1, C++ Primer Plus, 100.00000, 0
<== Total: 1
小李取出的商品价格:100.00000

==> Preparing: SELECT id,name,price,version FROM t_product WHERE id=?
==> Parameters: 1(Long)
<== Columns: id, name, price, version
<== Row: 1, C++ Primer Plus, 100.00000, 0
<== Total: 1
小王取出的商品价格:100.00000

==> Preparing: UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=?
==> Parameters: C++ Primer Plus(String), 150.00000(BigDecimal), 1(Long), 1(Long), 0(Long)
<== Updates: 1
小李修改商品价格的结果: true

==> Preparing: UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=?
==> Parameters: C++ Primer Plus(String), 70.00000(BigDecimal), 1(Long), 1(Long), 0(Long)
<== Updates: 0
小王修改商品价格的结果: false

最终的商品价格: 150.00000

优化测试代码

上面的测试代码可以加入更新重试机制,也就是在当前记录更新失败时,可以重新读取记录,然后再次尝试更新记录,示例代码如下:

1
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
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private ProductMapper productMapper;

@Test
public void updatePrice() {

Product product1 = productMapper.selectById(1L);
System.out.println("小李取出的商品价格:" + product1.getPrice());

Product product2 = productMapper.selectById(1L);
System.out.println("小王取出的商品价格:" + product2.getPrice());

product1.setPrice(product1.getPrice().add(new BigDecimal(50)));
int result1 = productMapper.updateById(product1);
System.out.println("小李修改商品价格的结果: " + (result1 > 0));

product2.setPrice(product2.getPrice().subtract(new BigDecimal(30)));
int result2 = productMapper.updateById(product2);
System.out.println("小王修改商品价格的结果: " + (result2 > 0));

if (result2 == 0) {
Product productNew = productMapper.selectById(1L);
productNew.setPrice(productNew.getPrice().subtract(new BigDecimal(30)));
int resultNew = productMapper.updateById(productNew);
System.out.println("小王第二次修改商品价格的结果: " + (resultNew > 0));
}

Product product3 = productMapper.selectById(1L);
System.out.println("最终的商品价格: " + product3.getPrice());
}

}

MyBatis-Plus 防全表更新与删除插件

BlockAttackInnerInterceptor 拦截器作用于 updatedelete 的 SQL 语句,可以阻止恶意的全表更新和全表删除操作。本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-09

防全表更新与删除插件的使用

防全表更新与删除插件的配置步骤

只需要使用 Spring XML 方式、 SpringBoot 配置类方式或者 MyBatis 配置文件方式中的任意一种配置防全表更新与删除插件即可。

配置防全表更新与删除插件

注入 MybatisPlusInterceptor 类,并配置 BlockAttackInnerInterceptor 拦截器。

Spring XML 方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
<property name="plugins">
<array>
<ref bean="mybatisPlusInterceptor"/>
</array>
</property>
</bean>

<bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors">
<list>
<ref bean="blockAttackInnerInterceptor"/>
</list>
</property>
</bean>

<bean id="blockAttackInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor"/>
SpringBoot 配置类方式
  • 配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* MyBatis-Plus 配置类
*/
@Configuration
@MapperScan("com.clay.mybatis.dao")
public class MybatisPlusConfig {

/**
* 添加 MyBatis-Plus 防全表更新与删除插件
*
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}

}
  • 启动类

提示

特殊情况下,可能还需要在启动类上通过 @ComponentScan 注解来扫描上面定义的 MybatisPlusConfig 配置类。

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@ComponentScan("com.clay.mybatis")
public class MyBatisPlusApplication {

public static void main(String[] args) {
SpringApplication.run(MyBatisPlusApplication.class, args);
}

}
MyBatis 配置文件方式
1
2
3
4
5
6
7
8
<configuration>
<plugins>
<!-- property 的配置说明详见 MybatisPlusInterceptor.setProperties() 的源码方法注释 -->
<plugin interceptor="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="@blockAttack" value="com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor"/>
</plugin>
</plugins>
</configuration>
Junit 单元测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private EmployeeMapper empMapper;

@Test
public void deleteAll() {
empMapper.delete(null);
}

@Test
public void updateAll() {
Employee employee = new Employee();
employee.setGender("1");
employee.setAge(26);
empMapper.update(employee, null);
}

}

执行上面的测试代码后,若抛出如下的异常信息,则说明防全表更新与删除插件生效了

1
2
3
4
5
6
7
8
9
10
11
12
13
Caused by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion
at com.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:49)
at com.baomidou.mybatisplus.core.toolkit.Assert.isTrue(Assert.java:38)
at com.baomidou.mybatisplus.core.toolkit.Assert.isFalse(Assert.java:50)
at com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor.checkWhere(BlockAttackInnerInterceptor.java:74)
at com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor.processDelete(BlockAttackInnerInterceptor.java:65)

Caused by: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation
at com.baomidou.mybatisplus.core.toolkit.ExceptionUtils.mpe(ExceptionUtils.java:49)
at com.baomidou.mybatisplus.core.toolkit.Assert.isTrue(Assert.java:38)
at com.baomidou.mybatisplus.core.toolkit.Assert.isFalse(Assert.java:50)
at com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor.checkWhere(BlockAttackInnerInterceptor.java:74)
at com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor.processUpdate(BlockAttackInnerInterceptor.java:70)