一、MyBatis 的插件机制
MyBatis Plus的插件机制也是基于MyBatis的插件机制;MyBatis通过插件Interceptor可以拦截四大组件相关方法的执行,完成相关数据的动态改变。这里所提到的MyBatis中的四大组件既:
- Executor
- StatementHandler
- ParameterHandler
- ResultSetHandler
这四个组件在创建时都会执行interceptorChain.pluginAll()方法,该方法会循环调用拦截器列表中每一个拦截器的plugin()方法,该方法会为四大组件创建并返回代理对象,从而可以通过代理对象进行方法拦截,达到增强目标方法的目的
以StatementHandler为例,BaseStatementHandler抽象类实现类StatementHandler接口,
BaseStatmentHandler包含了一个构造方法,构造方法中包含了parameterHandler属性,该属性通过newParameterHandler()方法创建
这里就是调用了pluginAll()方法
循环所有的拦截器,调用拦截器的plugin()方法,返回代理对象
创建工程
拷贝mybatis-plus-mpg项目重命名为mybatis-plus-interceptor
二、MyBatis Plus PaginationInnerInterceptor插件
MP的分页插件是PaginationInnerInterceptor,该接口实现了InnerInterceptor接口,MyBatisPlusInterceptor实现了Interceptor接口,MyBatisPlusInterceptor接口包含了一个InnerInterceptorList属性
并且实现类Interceptor接口的plugin()方法,plugin()方法中有调用了wrap()方法,该方法通过反射生成代理对象
这也就是为什么配置分页插件时要先配置一个InnerInterceptor
<!--配置分页插件--> <bean id="paginationInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"> <property name="dbType" value="MYSQL"></property> </bean> 复制代码
再配置一个Interceptor,并引用上面配置的InnerInterceptor
<!--配置拦截器--> <bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor"> <property name="interceptors" ref="paginationInnerInterceptor"></property> </bean> 复制代码
最后再将Interceptor配置到SQLSessionFactoryBean中
<!--替换为MyBatis-Plus的MyBatisSqlSessionFactoryBean,原来是MyBatis的SqlSessionFactoryBean--> <bean id="mybatisSqlSessionFactoryBean" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean"> <!--设置使用分页插件,否则分页会失效--> <property name="plugins" ref="mybatisPlusInterceptor"></property> </bean> 复制代码
在TeslaMapperTest中增加方法
@Test public void testPage(){ Page<Tesla> page = new Page<>(2, 4); Page<Tesla> teslaPage = teslaMapper.selectPage(page, null); System.out.println("获取总记录数:" + teslaPage.getRecords().size()); } 复制代码
selectPage()方法会返回一个Page对象,这个Page对象中包含分页相关的信息
System.out.println("----Page对象的属性和方法----"); System.out.println("获取总记录数:" + teslaPage.getTotal()); System.out.println("获取当前页面的记录:" + teslaPage.getRecords()); System.out.println("是否有下一页:" + teslaPage.hasNext()); System.out.println("是否有上一页:" + teslaPage.hasPrevious()); System.out.println("总页数为:" + teslaPage.getPages()); System.out.println("当前页数为:" + teslaPage.getCurrent()); System.out.println("当前页记录数为:" + teslaPage.getSize()); 复制代码
三、MyBatis Plus BlockAttackInnerInterceptor 防止全表更新与删除
在MP 3.4.0 版本之后SqlExplainInterceptor插件被删除,BlockAttackInnerInterceptor可以替代SQLExplainInterceptor来实现防止全表更新与删除的功能,具体可以参考官网 MyBatis Plus 插件主体
BlockAttackInnerInterceptor的作用是分析DELETE和UPDATE语句防止全表更新或者全表删除,适用于MySQL 5.6 版本以上,并且只建议在开发环境使用,不建议在生产环境使用
配置 BlockAttackInnerInterceptor 插件
配置interceptors拦截器列表,将分页插件和BlockAttackInnerInterceptor一起配置到mybatisPlusInterceptor的interceptors属性中,通过list标签来配置
<!--配置拦截器--> <bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor"> <property name="interceptors"> <list> <!--配置分页插件--> <bean id="paginationInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"> <property name="dbType" value="MYSQL"></property> </bean> <!--防止全表更新删除插件--> <bean id="blockAttackInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor"> </bean> </list> </property> </bean> 复制代码
测试插件
@Test public void testBlockAttackInnerInterceptor(){ // 全表删除 teslaMapper.delete(null); } 复制代码
SQL执行失败,成功阻止了全表删除操作,也可以参考官方文档中BlockAttackInnerInterceptor 的使用方式
四、MyBatis Plus IllegalSQLInnerInterceptor 不规范SQL拦截器插件
IllegalSQLInnerInterceptor插件可以对不规范的SQL进行拦截
<!--配置拦截器--> <bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor"> <property name="interceptors"> <list> <bean id="illegalSQLInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.IllegalSQLInnerInterceptor"> </bean> </list> </property> </bean> 复制代码
再次执行testPage()方法
此时会报错,因为SQL语句中没有WHERE关键字
该插件会拦截的SQL语句类型为:
- 必须使用到索引,包含left join连接字段,符合索引最左原则
- 如果因为动态SQL,bug导致update的where条件没有带上,全表更新上万条数据
- 如果检查到使用了索引,SQL性能基本不会太差
- SQL尽量单表执行,有查询left join的语句,必须在注释里面允许该SQL运行,否则会被拦截,有left join的语句,如果不能拆成单表执行的SQL,请leader商量在做,SQL尽量单表执行的好处:
- 查询条件简单、易于开理解和维护
- 扩展性极强;(可为分库分表做准备)
- 缓存利用率高
- where条件为空、!=、包含not、or、子查询,都会拦截
五、MyBatis Plus OptimisticLockerInnerInterceptor 乐观锁插件
什么是乐观锁?
乐观锁是对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突(一般实现方式是通过加版本号然后进行版本号的对比方式实现);
如果想实现如下需求,既当要更新一条记录时,希望这条记录没有被别人更新
乐观锁的实现原理:
- 取出记录时,获取当前的version
- 更新时带上这个version
- 执行更新时 version在原来的基础上+1
- 如果version不一致,更新失败
配置乐观锁插件
<!--配置拦截器--> <bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor"> <property name="interceptors"> <list> <!--其他插件配置省略--> <!--乐观锁插件--> <bean id="optimisticLockerInnerInterceptor" class="com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor"> </bean> </list> </property> </bean> 复制代码
给Tesla实体类添加Integer类型的version字段,并添加@Version注解,数据库表中添加对应的version中字段
TeslaMapperTest中增加测试代码
@Test public void testOptimisticLockerInnerInterceptor(){ // 更新操作 Tesla tesla = new Tesla(); tesla.setId(1166057518); tesla.setName("Model 3 2022"); tesla.setPrice(280000.00); tesla.setVehicleType("四门轿车"); tesla.setFactory("上海工厂"); tesla.setVersion(1); teslaMapper.updateById(tesla); } 复制代码
更新条件多了一个version字段,并且更新之后数据库中version字段从1变为2,
tesla.setFactory("上海工厂"); 复制代码
重新setFactory之后再次执行测试,此时代码中的version字段和数据库中的字段已经不一致
更新函数为0,更新失败