MyBaits Plus插件
# MyBaits Plus插件
MyBaits提供了插件机制,而MP提供了许多插件 (opens new window),对MyBatis进行增强。以下是3.4.0 版本+的配置方法。
# MyBatis的插件机制
MyBatis 允许你在已映射语句执⾏过程中的某⼀点进⾏拦截调⽤。默认情况下,MyBatis 允许使⽤插件来拦截的⽅法调⽤包括:
- 执行器-Executor (update、query、commit、rollback等方法);
- SQL语法构建器-StatementHandler (prepare、parameterize、batch、updates query等方法);
- 参数处理器-ParameterHandler (getParameterObject、setParameters方法);
- 结果集处理器-ResultSetHandler (handleResultSets、handleOutputParameters等方法);
简单来说,插件机制可以拦截执⾏器的⽅法、拦截参数的处理、拦截结果集的处理、拦截Sql语法构建的处理。详细的内容可以看这篇文章-MyBatis插件。
# MyBatis Plus插件
MP提供了许多插件 (opens new window),MybatisPlusInterceptor
是插件的核心,他有个List<InnerInterceptor> interceptors
属性,需要注入实现了InnerInterceptor的对象,目前提供的插件都将基于此接口来实现功能:
- 自动分页: PaginationInnerInterceptor
- 多租户: TenantLineInnerInterceptor
- 动态表名: DynamicTableNameInnerInterceptor
- 乐观锁: OptimisticLockerInnerInterceptor
- sql 性能规范: IllegalSQLInnerInterceptor
- 防止全表更新与删除: BlockAttackInnerInterceptor
我在使用的时候注意使用顺序,建议如下:
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
# 配置方式
以分页插件举例,更详细介绍可见官网分页插件 (opens new window)。
SpringBoot:
@Configuration
@MapperScan("org.example.hello.mp")
public class MybatisPlusConfig {
/**
* 新的分页插件
* 一缓和二缓遵循MyBatis的规则, 需要设置 MybatisConfiguration#useDeprecatedExecutor = false, 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// ... 加其他插件
return interceptor;
}
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}
Spring:
<bean id="configuration" class="com.baomidou.mybatisplus.core.MybatisConfiguration">
<!-- 需配置该值为false,避免1或2级缓存可能出现问题,该属性会在旧插件移除后一同移除 -->
<property name="useDeprecatedExecutor" value="false"/>
</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>
# 多租户插件
- 多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!
- 启用多租户后所有执行的method的sql都会进行处理.
- 自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
使用方式如下:
- Entity增加租户字段,user表增加
tenant_id
.
private Long tenantId;
- [可选的]实现TenantLineHandler接口
public interface TenantLineHandler {
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
Expression getTenantId();
/**
* 获取租户字段名
* <p>
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
default String getTenantIdColumn() {
// 如果该字段你不是固定的,请使用 SqlInjectionUtils.check 检查安全性
return "tenant_id";
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
default boolean ignoreTable(String tableName) {
return false;
}
}
这里我为了方便测试,实现了该接口:
public class MyTenantLineHandler implements TenantLineHandler {
@Override
public Expression getTenantId() {
// 固定租户ID=1
return new LongValue(1);
}
}
- 配置进interceptor
@Configuration
@MapperScan("org.example.hello.mp")
public class MybatisPlusConfig {
// ...
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new MyTenantLineHandler()));
// 分页插件 ...
return interceptor;
}
// ...
}
测试用例:初始化user部分数据的tenant_id = 1,然后开测。
@RunWith(SpringRunner.class)
@SpringBootTest
public class MPPluginsTest {
@Autowired
private UserMapper userMapper;
@Test
public void testTenant() {
List<User> allUser = userMapper.selectList(null);
// SELECT tenant_id, id, name, age, email FROM user WHERE tenant_id = 1
System.out.println(allUser);
}
}
# 动态表名插件
- 原理为解析替换设定表名为处理器的返回表名,表名建议可以定义复杂一些避免误替换。详见官方示例 (opens new window)
# 乐观锁插件
当要更新一条记录的时候,希望这条记录没有被别人更新。乐观锁实现方式:
- 取出记录时,获取当前 version
- 更新时,带上这个 version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果 version 不对,就更新失败
使用方式如下:
- Entity类字段加上
@Version
注解,标明为乐观锁版本字段。
@Version
private Integer version;
注意事项:
- 支持的数据类型只有:
int,Integer,long,Long,Date,Timestamp,LocalDateTime
- 整数类型下 newVersion = oldVersion + 1(必须先查出version,否则不会执行+1)
- newVersion 会回写到 entity 中
- 仅支持 updateById(id) 与 update(entity, wrapper) 方法
- 在 update(entity, wrapper) 方法下, wrapper 不能复用!!!
配置乐观锁插件到拦截链中
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 租户插件 ... // 乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 分页插件 ... return interceptor; }
测试用例:user表添加version字段,初始化值为1。
@Test
public void testUpdateVersion() {
// SELECT id, name, age, email, tenant_id, version FROM user WHERE id = 2 AND tenant_id = 1
User user = userMapper.selectById(2L);
user.setName("Jack2");
// 查询出来的user的version必须非空,不然不会+1
// UPDATE user SET name = 'Jack2', age = 20, email = 'test2@baomidou.com', tenant_id = 1, version = 2 WHERE tenant_id = 1 AND id = 2 AND version = 1
int i = userMapper.updateById(user);
// 1
System.out.println(i);
}
# SQL性能规范插件
由于开发人员水平参差不齐,即使订了开发规范很多人也不遵守,SQL是影响系统性能最重要的因素,所以拦截掉垃圾SQL语句。
个人建议是可以自己开发的时候使用,实际团队合作中可不开。
拦截SQL类型的场景
1.必须使用到索引,包含left join连接字段,符合索引最左原则
必须使用索引好处,
1.1 如果因为动态SQL,bug导致update的where条件没有带上,全表更新上万条数据
1.2 如果检查到使用了索引,SQL性能基本不会太差
2.SQL尽量单表执行,有查询left join的语句,必须在注释里面允许该SQL运行,否则会被拦截,有left join的语句,如果不能拆成单表执行的SQL,请leader商量在做
https://gaoxianglong.github.io/shark
SQL尽量单表执行的好处
2.1 查询条件简单、易于开理解和维护;
2.2 扩展性极强;(可为分库分表做准备)
2.3 缓存利用率高;
2.在字段上使用函数
3.where条件为空
4.where条件使用了 !=
5.where条件使用了 not 关键字
6.where条件使用了 or 关键字
7.where条件使用了 使用子查询
配置性能规范插件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// ... 乐观锁插件、分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 性能规范插件
interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
return interceptor;
}
测试用例:注意先删除租户插件配置。
@Test
public void testIllegalSQL() {
User user = new User();
user.setName("illegal test");
userMapper.update(user, null);
}
// Error updating database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: 非法SQL,必须要有where条件
# 防止全表更新与删除
阻断全表更新、删除的操作。
- 直接配置BlockAttackInnerInterceptor拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 防止全表更新与删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
return interceptor;
}
测试用例:注意先删除租户插件、性能规范插件配置。
@Test
public void testDelAndUpdateAll() {
User user = new User();
user.setName("illegal test");
userMapper.update(user, null);
// Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update operation
userMapper.delete(null);
// Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion
}
上次更新: 5/30/2023, 11:23:39 PM