一、前言

MybatisPlusInterceptor

MybatisPlusInterceptor:该插件是核心插件,目前代理了 Executor#queryExecutor#updateStatementHandler#prepare 方法。

官方文档:插件使用

InnerInterceptor

我们提供的插件都将基于此接口来实现功能

目前已有的功能:

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

注意:使用多个功能需要注意顺序关系,建议使用如下顺序

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

总结: 对 sql 进行单次改造的优先放入,不对 sql 进行改造的最后放入

二、分页插件

MyBatis Plus 自带分页插件,只要简单的配置即可实现分页功能

1、添加配置类

创建config 包,创建MybatisPlusConfig 类

类中添加 @Bean 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@MapperScan("com.atguigu.mybatisplus.mapper")
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
// 针对MYSQL做分页
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

2、测试分页

创建类InterceptorTests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
public class InterceptorTests {
@Resource
private UserMapper userMapper;

@Test
public void testSelectPage() {
// 分页参数对象
Page<User> pageParam = new Page<>(1, 5);
// selectPage接收两个对象:分页参数对象,查询对象
userMapper.selectPage(pageParam, null);

// ==> Preparing: SELECT uid AS id,name,age,email,create_time,update_time,is_deleted AS deleted FROM t_user WHERE is_deleted=0 LIMIT ?
// ==> Parameters: 5(Long)
// 两个 pageParam 是同一个对象
List<User> users = pageParam.getRecords();
users.forEach(System.out::println);
}
}

3、XML 自定义分页

UserMapper中定义接口方法,实现自定义分页功能

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.mybatisplus.mapper;

public interface UserMapper extends BaseMapper<User> {

/**
* 查询 : 根据年龄查询用户列表,分页显示
*
* @param page 分页对象,xml中可以从里面进行取值,传递参数 Page ,即自动分页,必须放在第一位
* @param age 年龄
* @return 分页对象
*/
IPage<User> selectPageByAge(Page<?> page, Integer age);
}

定义 XML

1
2
3
4
5
6
7
8
9
10
<sql id="Base_Column_List">
uid,name,age,email,is_deleted,create_time,update_time
</sql>

<select id="selectPageByAge" resultType="com.atguigu.mybatisplus.entity.User">
select
<include refid="Base_Column_List"/>
from t_user
where age > #{age}
</select>

测试(总记录数为 0,MP 不进行分页查询,比如查询年龄大于 200 的)

1
2
3
4
5
6
7
8
9
10
@Test
public void testSelectPageByAge() {
Page<User> userParam = new Page<>(1, 5);
// ==> Preparing: SELECT COUNT(*) FROM t_user WHERE age > ?
// ==> Parameters: 20(Integer)
userMapper.selectPageByAge(userParam, 20);

List<User> users = userParam.getRecords();
users.forEach(System.out::println);
}

4、补充

字段映射的两种解决方法

UserMapper中使用resultMap做映射

1
2
3
4
<resultMap id="myUser" type="com.atguigu.mybatisplus.entity.User">
<!-- 数据库表中uid映射为实体类属性中的id-->
<id column="uid" property="id"/>
</resultMap>

或者直接起别名

1
2
3
4
5
6
7
8
9
<sql id="Base_Column_List">
uid as id,
name,
age,
email,
is_deleted as deleted,
create_time as createTime,
update_time as updateTime
</sql>

三、乐观锁

1、场景

一件商品,成本价是 80 元,售价是 100 元。老板先是通知小李,说你去把商品价格增加 50 元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到 150 元,价格太高,可能会影响销量。又通知小王,你把商品价格降低 30 元。

此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格 100 元;小王也在操作,取出的商品价格也是 100 元。小李将价格加了 50 元,并将 100+50=150 元存入了数据库;小王将商品减了 30 元,并将 100-30=70 元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。

现在商品价格是 70 元,比成本价低 10 元。几分钟后,这个商品很快出售了 1 千多件商品,老板亏 1 万多。

接下来将我们演示这一过程:

第一步:数据库中增加商品表

1
2
3
4
5
6
7
8
9
10
CREATE TABLE product
(
id BIGINT(20) NOT NULL AUTO_INCREMENT 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 product (id, NAME, price) VALUES (1, '笔记本', 100);

第二步:创建实体类

1
2
3
4
5
6
7
8
9
package com.atguigu.mybatisplus.entity;

@Data
public class Product {
private Long id;
private String name;
private Integer price;
private Integer version;
}

第三步:创建 Mapper

1
2
3
4
5
package com.atguigu.mybatisplus.mapper;

public interface ProductMapper extends BaseMapper<Product> {

}

第四步:测试

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
@Resource
private ProductMapper productMapper;

@Test
public void testConcurrentUpdate() {
// 小李取数据
Product p1 = productMapper.selectById(1l);

// 小王取数据
Product p2 = productMapper.selectById(1L);

// 小李修改数据 + 50
p1.setPrice(p1.getPrice() + 50);
productMapper.updateById(p1);
System.out.println("小李修改的结果" + p1.getPrice());

// 小王取数据 -30
p2.setPrice(p2.getPrice() - 30);
productMapper.updateById(p2);
System.out.println("小王修改的结果" + p2.getPrice());

// 老板看价格
Product p3 = productMapper.selectById(1L);
System.out.println("老板看到的价格" + p3.getPrice());
}

2、乐观锁方案原理

  • 数据库中添加 version 字段:取出记录时,获取当前 version
  • 更新时,version + 1,如果 where 语句中的 version 版本不对,则更新失败

3、乐观锁实现流程

第一步:修改实体类

version属性上添加 @Version 注解

1
2
3
4
5
6
7
8
9
@Data
public class Product {
private Long id;
private String name;
private Integer price;

@Version
private Integer version;
}

第二步:在配置类里添加乐观锁插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@MapperScan("com.atguigu.mybatisplus.mapper")
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
// 针对MYSQL做分页
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}

重新执行测试

小王的修改失败,老板看到价格为 150

4、优化流程

失败后重试

1
2
3
4
5
6
7
8
9
10
11
12
13
// 小王取数据 -30
p2.setPrice(p2.getPrice() - 30);
int result = productMapper.updateById(p2);
System.out.println("小王修改的结果" + p2.getPrice());

// 失败后重试
if (result == 0) {
//重新获取数据
p2 = productMapper.selectById(1L);
//更新
p2.setPrice(p2.getPrice() - 30);
productMapper.updateById(p2);
}

测试,最后老板看到的价格 120