• 【原创】为MybatisPlus增加一个逻辑删除插件,让XML中的SQL也能自动增加逻辑删除功能


    前言

    看到这个标题有人就要说了,D哥啊,MybatisPlus不是本来就有逻辑删除的配置吗,比如@TableLogic注解,配置文件里也能添加如下配置设置逻辑删除。

    1. mybatis-plus:
    2. mapper-locations: classpath*:mapper/*.xml
    3. configuration:
    4. mapUnderscoreToCamelCase: true
    5. global-config:
    6. db-config:
    7. logic-delete-field: del
    8. logic-delete-value: 1
    9. logic-notDelete-value: 0

    但是我想说,xml中添加了逻辑删除了吗?很明显这个没有,MybatisPlus只在QueryWrapper中做了手脚,而xml是Mybatis的功能,非MybatisPlus的功能。而xml中写SQL又是我工作中最常用到的,优势在于SQL可读性强,结合MybatisX插件并在IDEA中连接database后能够直接跳转到方法和表,且对多表join和子查询支持都比QueryWrapper来得好。而逻辑删除又是会经常漏掉的字段,虽然说手动添加也不费多少时间,但是麻烦的是容易漏掉,特别是子查询和join的情况,而且时间积少成多,我觉得有必要解决这个问题。

    本插件适用于绝大部分表都拥有逻辑删除字段的情况!!

    本文使用的MybatisPlus的版本为3.5.3.1

    不多说了,直接上代码!

    1. /**
    2. * @author DCT
    3. * @version 1.0
    4. * @date 2023/11/9 23:10:18
    5. * @description
    6. */
    7. @Slf4j
    8. public class DeleteMpInterceptor implements InnerInterceptor {
    9. public static final String LOGIC_DELETE = "LOGIC_DELETE";
    10. public static final List excludeFunctions = new ArrayList<>();
    11. protected String deleteFieldName;
    12. public DeleteMpInterceptor(String deleteFieldName) {
    13. this.deleteFieldName = deleteFieldName;
    14. }
    15. static {
    16. excludeFunctions.add("selectList");
    17. excludeFunctions.add("selectById");
    18. excludeFunctions.add("selectBatchIds");
    19. excludeFunctions.add("selectByMap");
    20. excludeFunctions.add("selectOne");
    21. excludeFunctions.add("selectCount");
    22. excludeFunctions.add("selectObjs");
    23. excludeFunctions.add("selectPage");
    24. excludeFunctions.add("selectMapsPage");
    25. }
    26. @SneakyThrows
    27. @Override
    28. public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    29. if (InterceptorIgnoreHelper.willIgnoreOthersByKey(ms.getId(), LOGIC_DELETE)) {
    30. return;
    31. }
    32. if (StringUtils.isBlank(deleteFieldName)) {
    33. // INFO: Zhouwx: 2023/11/20 没有设置逻辑删除的字段名,也忽略添加逻辑删除
    34. return;
    35. }
    36. PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
    37. String sql = mpBs.sql();
    38. Statement statement = CCJSqlParserUtil.parse(sql);
    39. if (!(statement instanceof Select)) {
    40. return;
    41. }
    42. String id = ms.getId();
    43. int lastIndexOf = id.lastIndexOf(".");
    44. String functionName = id.substring(lastIndexOf + 1);
    45. if (excludeFunctions.contains(functionName)) {
    46. // INFO: DCT: 2023/11/12 QueryWrapper的查询,本来就会加逻辑删除,不需要再加了
    47. return;
    48. }
    49. Select select = (Select) statement;
    50. SelectBody selectBody = select.getSelectBody();
    51. // INFO: DCT: 2023/11/12 处理核心业务
    52. handleSelectBody(selectBody);
    53. // INFO: DCT: 2023/11/12 将处理完的数据重新转为MPBoundSql
    54. String sqlChange = statement.toString();
    55. mpBs.sql(sqlChange);
    56. }
    57. protected void handleSelectBody(SelectBody selectBody) {
    58. if (selectBody instanceof PlainSelect) {
    59. PlainSelect plainSelect = (PlainSelect) selectBody;
    60. // INFO: DCT: 2023/11/12 处理join中的内容
    61. handleJoins(plainSelect);
    62. Expression where = plainSelect.getWhere();
    63. FromItem fromItem = plainSelect.getFromItem();
    64. EqualsTo equalsTo = getEqualTo(fromItem);
    65. if (where == null) {
    66. // INFO: DCT: 2023/11/12 where条件为空,增加一个where,且赋值为 表名.del = 0
    67. plainSelect.setWhere(equalsTo);
    68. } else {
    69. // INFO: DCT: 2023/11/12 普通where,后面直接加上本表的 表名.del = 0
    70. AndExpression andExpression = new AndExpression(where, equalsTo);
    71. plainSelect.setWhere(andExpression);
    72. // INFO: DCT: 2023/11/12 有子查询的处理子查询
    73. handleWhereSubSelect(where);
    74. }
    75. }
    76. }
    77. /**
    78. * 这一段来自MybatisPlus的租户插件源码,通过递归的方式加上需要的SQL
    79. *
    80. * @param where
    81. */
    82. protected void handleWhereSubSelect(Expression where) {
    83. if (where == null) {
    84. return;
    85. }
    86. if (where instanceof FromItem) {
    87. processOtherFromItem((FromItem) where);
    88. return;
    89. }
    90. if (where.toString().indexOf("SELECT") > 0) {
    91. // 有子查询
    92. if (where instanceof BinaryExpression) {
    93. // 比较符号 , and , or , 等等
    94. BinaryExpression expression = (BinaryExpression) where;
    95. handleWhereSubSelect(expression.getLeftExpression());
    96. handleWhereSubSelect(expression.getRightExpression());
    97. } else if (where instanceof InExpression) {
    98. // in
    99. InExpression expression = (InExpression) where;
    100. Expression inExpression = expression.getRightExpression();
    101. if (inExpression instanceof SubSelect) {
    102. handleSelectBody(((SubSelect) inExpression).getSelectBody());
    103. }
    104. } else if (where instanceof ExistsExpression) {
    105. // exists
    106. ExistsExpression expression = (ExistsExpression) where;
    107. handleWhereSubSelect(expression.getRightExpression());
    108. } else if (where instanceof NotExpression) {
    109. // not exists
    110. NotExpression expression = (NotExpression) where;
    111. handleWhereSubSelect(expression.getExpression());
    112. } else if (where instanceof Parenthesis) {
    113. Parenthesis expression = (Parenthesis) where;
    114. handleWhereSubSelect(expression.getExpression());
    115. }
    116. }
    117. }
    118. /**
    119. * 处理子查询等
    120. */
    121. protected void processOtherFromItem(FromItem fromItem) {
    122. // 去除括号
    123. while (fromItem instanceof ParenthesisFromItem) {
    124. fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
    125. }
    126. if (fromItem instanceof SubSelect) {
    127. SubSelect subSelect = (SubSelect) fromItem;
    128. if (subSelect.getSelectBody() != null) {
    129. // INFO: Zhouwx: 2023/11/20 递归从select开始查找
    130. handleSelectBody(subSelect.getSelectBody());
    131. }
    132. } else if (fromItem instanceof ValuesList) {
    133. log.debug("Perform a subQuery, if you do not give us feedback");
    134. } else if (fromItem instanceof LateralSubSelect) {
    135. LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
    136. if (lateralSubSelect.getSubSelect() != null) {
    137. SubSelect subSelect = lateralSubSelect.getSubSelect();
    138. if (subSelect.getSelectBody() != null) {
    139. // INFO: Zhouwx: 2023/11/20 递归从select开始查找
    140. handleSelectBody(subSelect.getSelectBody());
    141. }
    142. }
    143. }
    144. }
    145. protected void handleJoins(PlainSelect plainSelect) {
    146. List joins = plainSelect.getJoins();
    147. if (joins == null) {
    148. return;
    149. }
    150. for (Join join : joins) {
    151. // INFO: DCT: 2023/11/12 获取表别名,并获取 表.del = 0
    152. FromItem rightItem = join.getRightItem();
    153. EqualsTo equalsTo = getEqualTo(rightItem);
    154. // INFO: DCT: 2023/11/12 获取on右边的表达式
    155. Expression onExpression = join.getOnExpression();
    156. // INFO: DCT: 2023/11/12 给表达式增加 and 表.del = 0
    157. AndExpression andExpression = new AndExpression(onExpression, equalsTo);
    158. ArrayList expressions = new ArrayList<>();
    159. expressions.add(andExpression);
    160. // INFO: DCT: 2023/11/12 目前的这个表达式就是and后的表达式了,不用再增加原来的表达式,因为这个and表达式已经包含了原表达式所有的内容了
    161. join.setOnExpressions(expressions);
    162. }
    163. }
    164. protected EqualsTo getEqualTo(FromItem fromItem) {
    165. Alias alias = fromItem.getAlias();
    166. String aliasName = "";
    167. if (alias == null) {
    168. if (fromItem instanceof Table) {
    169. Table table = (Table) fromItem;
    170. aliasName = table.getName();
    171. }
    172. } else {
    173. aliasName = alias.getName();
    174. }
    175. EqualsTo equalsTo = new EqualsTo();
    176. Column leftColumn = new Column();
    177. leftColumn.setColumnName(aliasName + "." + deleteFieldName);
    178. equalsTo.setLeftExpression(leftColumn);
    179. equalsTo.setRightExpression(new LongValue(0));
    180. return equalsTo;
    181. }
    182. }

    代码说明

    这代码中已经有很多注释了,其实整段代码并非100%我的原创,而是借鉴了MybatisPlus自己的TenantLineInnerIntercept,因为两者的功能实际上非常详尽,我依葫芦画瓢造了一个,特别是中间的handleWhereSubSelect方法,令人拍案叫绝!使用大量递归,通过非常精简的代码处理完了SQL查询中所有的子查询。

    getEqualTo方法可以算是逻辑删除插件的核心代码了,先判断表是否存在别名,如果有,就拿别名,如果没有就拿表名,防止出现多个表都有逻辑删除字段的情况下指代不清的情况,出现查询ambiguous错误。通过net.sf.jsqlparser中的EqualsTo表达式,拼上字段名和未被逻辑删除的值0(这里可以按照自己的情况进行修改!)

    顶上的excludeFunctions是为了排除QueryWrapper的影响,因为QueryWrapper自己会加上逻辑删除,而这个插件还会再添加一个逻辑删除,导致出现重复,打印出来的SQL不美观。

    使用方法

    和MybatisPlus其他的插件一样,都通过@Bean的方式进行配置

    1. @Value("${mybatis-plus.global-config.db-config.logic-delete-field}")
    2. private String deleteFieldName;
    3. @Bean
    4. public MybatisPlusInterceptor mybatisPlusInterceptor() {
    5. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    6. interceptor.addInnerInterceptor(new DeleteMpInterceptor(deleteFieldName));
    7. return interceptor;
    8. }

    我这里的deleteFieldName直接借用了MybatisPlus自己的逻辑删除配置,可以自己设置

    排除使用这个插件

    如果有几张表是没有逻辑删除字段的,那么这个插件自动补的逻辑删除字段则会导致SQL出现报错,可以通过添加以下注解@InterceptorIgnore排除插件对于该方法的生效

    1. @Repository
    2. interface ExampleMapper : BaseMapper<ExampleEntity> {
    3. @InterceptorIgnore(others = [DeleteMpInterceptor.LOGIC_DELETE + "@true"])
    4. fun findByList(query: ExampleQo): List
    5. }

    上面的代码是Kotlin写的,但是不妨碍查看

    运行效果

    mapper中代码为:

    1. /**
    2. * @author DCTANT
    3. * @version 1.0
    4. * @date 2023/11/20 17:40:41
    5. * @description
    6. */
    7. @Repository
    8. interface ExampleMapper : BaseMapper<ExampleEntity> {
    9. fun findByList(query: ExampleQo): List
    10. }

    xml中的代码为:

    1. <select id="findByList" resultType="com.itdct.server.admin.example.vo.ExampleListVo">
    2. select t.* from test_example as t
    3. <where>
    4. <if test="name != null and name != ''">and t.name = #{name}if>
    5. <if test="number != null">and t.number = #{number}if>
    6. <if test="keyword != null and keyword != ''">and t.name like concat('%',#{keyword},'%')if>
    7. <if test="startTime != null">and t.create_time > #{startTime}if>
    8. <if test="endTime != null">and t.create_time < #{endTime}if>
    9. where>
    10. order by t.create_time desc
    11. select>

    执行效果为:

    t.del = 0就是逻辑删除插件添加的代码,当然join和子查询我也使用了,目前来看没有什么问题

    欢迎大家提出修改意见

    目前这个代码还处于Demo阶段,并没有上线使用,还是处于没充分测试的状态,欢迎大家提出整改意见!如果有bug我也会及时修复。

  • 相关阅读:
    git stash详解
    java多线程-线程间通信
    分类网络搭建示例
    数据结构 - 单链表
    Spring Cloud Gateway-系统保护Sentinel集成
    Virtio Over PCI Bus
    计算属性全选反选
    operator-sdk入门(mac)
    web前端期末大作业——HTML+CSS简单的旅游网页设计与实现
    Spring Boot 2.6.0正式发布:默认禁止循环依赖、增强Docker镜像构建...
  • 原文地址:https://blog.csdn.net/DCTANT/article/details/134515180