Mybatis-Plus 3.4.x版本多租户关联SQL查询 clause is ambiguou异常

810 阅读6分钟

Mybatis-Plus 3.4.x版本多租户关联SQL查询异常

Column 'tenant_id' in where clause is ambiguous异常问题处理

提示:如果时间不够请忽略我下边啰里啰嗦的背景及分析问题的过程,直接跳到解决问题这一步!!!

背景

近期,公司组织安排代码漏洞扫描,暴露出许多问题,其中,最主要的便是springboot(2.1.10.RELEASE)以及springcloud(Greenwich.SR4)版本太低,导致许多相应组件存在许多漏洞。考虑到近期股份公司要进行网络攻防演练,公司领导决定升级程序版本,解决存在的许多已知漏洞,进而引出了此次问题。

在版本升级的前提下,需要升级Mybatis-plus(3.2.0)到3.4.2版本,由于公司业务存在多租户的情况,自然而然的使用了Mybatis-plus的多租户插件。公司在前期需求分析时就引入了多租户的概念,所以在数据库表结构设计时,就预留了此字段,但是,在后期的代码编写阶段,许多开发人员并没有严格按照开发文档执行,有许多插入SQL中手动拼接了租户ID(tenant_id)字段,导致后期启用多租户插件后发现在插入语句中tenant_id字段重复。

所以,想要删除代码中手动拼接的租户ID字段不仅费时费力,而且不一定能够全部的筛选处理,所以,为了解决插入SQL语句中重复的租户ID字段问题,我们继承了TenantSqlParser类,将TenantSqlParser的processInsert方法重写,通过解析Insert类下的columns字段,判断是否已经包含租户ID字段,如果已经存在了,那么就不会通过多租户插件进行自动拼接了。实现方法下面会提到。

同时,在查询语句中,因为mybatis-plus自带的功能只会拼接left 、from和where后面的表或子查询添加租户id,但是,当Select中存在子查询时,并没有自动拼接租户ID,所以,为了实现子查询自动拼接租户ID,我们也将TenantSqlParser的processPlainSelect方法进行重写,处理Select部分的嵌套查询拼接租户id问题。

TenantSqlParser重写代码:

 import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
 import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
 import net.sf.jsqlparser.expression.CaseExpression;
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.Function;
 import net.sf.jsqlparser.expression.Parenthesis;
 import net.sf.jsqlparser.expression.WhenClause;
 import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
 import net.sf.jsqlparser.expression.operators.relational.ItemsList;
 import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
 import net.sf.jsqlparser.schema.Column;
 import net.sf.jsqlparser.statement.insert.Insert;
 import net.sf.jsqlparser.statement.select.PlainSelect;
 import net.sf.jsqlparser.statement.select.SelectBody;
 import net.sf.jsqlparser.statement.select.SelectExpressionItem;
 import net.sf.jsqlparser.statement.select.SelectItem;
 import net.sf.jsqlparser.statement.select.SubSelect;
 ​
 import java.util.List;
 ​
 /***
 * 多租户sql解析器扩展
 * @description: 多租户sql解析器扩展
 * @author: nilong
 * @date: 2022/5/16 11:10
 * @return:
 */
 public class TenantSqlParserExt extends TenantSqlParser {
 ​
     private static final String IF_NULL = "IFNULL";
     /**
      * 是否处理插入的sql
      */
     private boolean isHandlerInsert = true;
 ​
     /**
      * 是否处理查询的sql
      */
     private boolean isHandlerSelect = false;
 ​
     @Override
     public void processSelectBody(SelectBody selectBody) {
         if(isHandlerSelect){
             super.processSelectBody(selectBody);
         }
     }
     @Override
     public void processInsert(Insert insert) {
         if (getTenantHandler().doTableFilter(insert.getTable().getName())) {
             // 过滤退出执行
             return;
         }
         boolean isExistTenantId =isExistTenantColumn(insert);
         if (isExistTenantId) {
             return;
         }
         insert.getColumns().add(new Column(getTenantHandler().getTenantIdColumn()));
         if (insert.getSelect() != null) {
             processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
         } else if (insert.getItemsList() != null) {
             // fixed github pull/295
             ItemsList itemsList = insert.getItemsList();
             if (itemsList instanceof MultiExpressionList) {
                 ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(getTenantHandler().getTenantId(false)));
             } else {
                 ((ExpressionList) insert.getItemsList()).getExpressions().add(getTenantHandler().getTenantId(false));
             }
         } else {
             throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
         }
     }
 ​
     /**
      * 重写processPlainSelect方法
      * 处理select部分的子查询拼接租户id
      * @param plainSelect
      */
     @Override
     protected void processPlainSelect(PlainSelect plainSelect) {
         // SELECT 至 FROM 中的嵌套查询
         List<SelectItem> selectItemList = plainSelect.getSelectItems();
         for (SelectItem selectItem : selectItemList) {
             if (selectItem instanceof SelectExpressionItem) {
                 SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
                 if (!(selectExpressionItem.getExpression() instanceof Column)) {
                     // 处理 column select 嵌套部分
                     operateExpression(selectExpressionItem.getExpression());
                 }
             }
         }
         processPlainSelect(plainSelect, false);
     }
 ​
     /**
      * 处理 column select 嵌套部分
      * @param expression
      */
     private void operateExpression(Expression expression){
         //例如:select (slect a from b where b.id=table.bid)a,table.* from table
         if (expression instanceof SubSelect){
             SubSelect subSelect = (SubSelect) expression;
             PlainSelect plainSelect = (PlainSelect) subSelect.getSelectBody();
             processSelectBody(plainSelect);
         } else if (expression instanceof Parenthesis){
             Parenthesis parenthesis= (Parenthesis) expression;
             operateExpression(parenthesis.getExpression());
         }else if (expression instanceof CaseExpression) {
             //处理case when
             CaseExpression caseExpression= (CaseExpression) expression;
             caseExpression.getWhenClauses();
             List<WhenClause> whenClauses = caseExpression.getWhenClauses();
             for (Expression e : whenClauses) {
                 if (e instanceof WhenClause){
                     WhenClause whenClause= (WhenClause) e;
                     operateExpression(whenClause.getThenExpression());
                 }
             }
         }else if (expression instanceof Function){
             //处理IFNULL问题
             Function function= (Function) expression;
             if (IF_NULL.equals(function.getName())){
                 ExpressionList expressionList=function.getParameters();
                 List<Expression> ifExpression=expressionList.getExpressions();
                 for (Expression e:ifExpression){
                     operateExpression(e);
                 }
             }
         }
     }
     /**
      * 判断是否存在租户id列字段
      * @param insert
      * @return 如果已经存在,则绕过不执行
      */
     private boolean isExistTenantColumn(Insert insert) {
         List<Column> columns = insert.getColumns();
         if(CollectionUtils.isEmpty(columns)){
             return false;
         }
         String tenantIdColumn = getTenantHandler().getTenantIdColumn();
         return columns.stream().map(Column::getColumnName).anyMatch(tenantId -> tenantId.equals(tenantIdColumn));
     }
 }
 ​

好了,废话啰嗦够了,可以进入正题了,哈哈哈哈哈

1. 分析问题

升级了Mybatis-plus(3.4.2)版本后,连接查询(Join)带有租户ID字段的表会报会报Column 'tenant_id' in where clause is ambiguous,这个异常很明显,租户ID这个字段不知道是哪一个表的字段,类似这样:

 SELECT
     A.*
 FROM
     A
     LEFT JOIN B ON B.a_id = A.id 
     AND tenant_id = 1 
 WHERE
     rp_rent_in_info.company_name LIKE "%"
     AND tenant_id = 1

解决方法很简单,这里需要将LEFT、WHERE条件后都追加上表名。类似这样:

 SELECT
     A.*
 FROM
     A
     LEFT JOIN B ON B.a_id = A.id 
     AND B.tenant_id = 1 
 WHERE
     rp_rent_in_info.company_name LIKE "%"
     AND A.tenant_id = 1

但是,按理说这么简单的问题Mybatis-plus不可能不会处理,所以,我翻看了Mybatis-plus官网关于多租户插件的介绍。果然,官网已经给了明确的说明

image-20230802154728432

同时,翻看Mybatis-plus源码可以更清楚的明白,为什么要给表增加别名,这里贴上源码的方法:

 protected Column getAliasColumn(Table table) {
         StringBuilder column = new StringBuilder();
         if (table.getAlias() != null) {
             column.append(table.getAlias().getName()).append(".");
         }
 ​
         column.append(this.tenantLineHandler.getTenantIdColumn());
         return new Column(column.toString());
     }
 ​

第二个方法的意思是为了获取别名字段列,如果表存在别名,那么拼接后的SQL为别名.租户ID,如果没有别名,就只是租户ID,就和上边提到的SQL一毛一样。

提问:

为什么3.2.0版本就没有问题呢?

我们翻看一下3.2.0版本源码,发现这里mybatis-plus已经帮我们处理了,如果不存在别名的情况下,会使用表名进行拼接。

     protected Column getAliasColumn(Table table) {
         StringBuilder column = new StringBuilder();
         if (null == table.getAlias()) {
             column.append(table.getName());
         } else {
             column.append(table.getAlias().getName());
         }
 ​
         column.append(".");
         column.append(this.tenantHandler.getTenantIdColumn());
         return new Column(column.toString());
     }

为什么高版本就不进行处理了呢?

按理说,大多数在不影响整体的实现上,高版本会向低版本兼容的,这里为什么就没有再进行处理呢?

我们同样的翻看了3.4.2版本源码,发现这个类已经被弃用了

image-20230802165950536

官网已经明确的说明不再使用这个类,取而代之的是TenantLineInnerInterceptor类。但是,我们之前也翻看了3.4.2版本源码,这个TenantLineInnerInterceptor类下的getAliasColumn依然是没有处理不存在别名的情况,不知道Mybatis-plus是不是为了让我们规范代码而故意为之呢。哈哈哈哈哈哈,狗头保命!

好了,既然知道了报错的原因,那么也就有了解决方法。

2. 解决问题

我们知道了是因为SQL书写不规范导致的,那么解决方法就是给所有的SQL表增加别名!但是,由于业务表实在是太多,挨个识别下来工作量太大,我们能不能想一个简单通用的方法呢?

这就要提到上边我们说的mybatis-plus自带的功能只会拼接left 、from和where后面的表或子查询添加租户id的问题了,之前我们将查询方法进行了重写,那么,我们能不能在查询方法中,在拼接租户ID之前,将不存在别名的表赋值一个别名呢?说干就干,我们首先找到之前处理select嵌套子查询的地方,也就是processPlainSelect方法,增加对表别名的修改:

 /**
      * 重写processPlainSelect方法
      * 处理select部分的子查询拼接租户id
      *
      * @param plainSelect
      */
     @Override
     protected void processPlainSelect(PlainSelect plainSelect) {
         //处理from表别名问题
         FromItem fromItem = plainSelect.getFromItem();
         if (fromItem instanceof Table) {
             Table fromTable = (Table) fromItem;
             if (null == fromTable.getAlias()) {
                 Alias alias = new Alias(fromTable.getName());
                 // fromTable.setAlias(alias);
                 plainSelect.getFromItem().setAlias(alias);
             }
         }
         //处理左右关联表别名问题
         List<Join> selectJoins = plainSelect.getJoins();
         if (CollectionUtils.isNotEmpty(selectJoins)) {
             for (Join selectJoin : selectJoins) {
                 FromItem rightItem = selectJoin.getRightItem();
                 Table fromTable = (Table) rightItem;
                 if (null == fromTable.getAlias()) {
                     Alias alias = new Alias(fromTable.getName());
                     // fromTable.setAlias(alias);
                     rightItem.setAlias(alias);
                 }
             }
         }
         // SELECT 至 FROM 中的嵌套查询
         List<SelectItem> selectItemList = plainSelect.getSelectItems();
         for (SelectItem selectItem : selectItemList) {
             if (selectItem instanceof SelectExpressionItem) {
                 SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
                 if (!(selectExpressionItem.getExpression() instanceof Column)) {
                     // 处理 column select 嵌套部分
                     operateExpression(selectExpressionItem.getExpression());
                 }
             }
         }
         processPlainSelect(plainSelect, false);
     }

至此,问题解决。