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官网关于多租户插件的介绍。果然,官网已经给了明确的说明
同时,翻看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版本源码,发现这个类已经被弃用了
官网已经明确的说明不再使用这个类,取而代之的是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);
}
至此,问题解决。