记Mybatis-plus多租户插件引起sql异常的问题解决过程
前文
本篇大多数文本描述的是问题发生的始末及解决过程,想直接看解决方案的可以点击此处跳转。
问题背景
不久前收到一个需求,要求做两套一模一样的功能,这就要求进行数据隔离,通常的数据隔离方案有:
独立数据库
共享数据库,独立Schema(如Oracle、DB2等)
共享数据库,共享 Schema,共享数据表
因为我们使用的Mysql
数据库,所以方案2
首先被排除,使用方案1
需引入多数据源组件,并新建数据库,因为这一个小功能分出两个库显然也有点儿大材小用,最终决定使用方案3
,落地方案为引入Mybatis-plus多租户插件。
注: 已知目前项目依赖的mybatis-plus
版本为3.1.2
, jsqlParser
版本为2.0
。
问题发生
因为我们是分布式架构,每个业务一个模块,而这个业务功能归属到一个已有模块业务,考虑到加入的多租户插件不能影响到同模块其他业务功能,因此进行了表过滤,多租户处理器相关代码如下:
然后Mybatis-plus配置里加入此处理器:
标2ProjectHandler
是上面提到的处理器,进行多租户表过滤,标1TenantSqlParser
依赖了此处理器,这个TenantSqlParser
大家注意下,后续分析原因会提到。
随后功能正常上线,但是上线后不久,发现同模块老功能的某个sql执行异常了,异常如下:
Exception in thread net.sf.jsqlparser.JSQLParserException
at net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatements(CCJSqlParserUtil.java:123)
Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "left" "LEFT"
at line 2, column 1.
Was expecting one of:
","
"SET"
at net.sf.jsqlparser.parser.CCJSqlParser.generateParseException(CCJSqlParser.java:20872)
at net.sf.jsqlparser.parser.CCJSqlParser.jj_consume_token(CCJSqlParser.java:20722)
at net.sf.jsqlparser.parser.CCJSqlParser.Update(CCJSqlParser.java:731)
at net.sf.jsqlparser.parser.CCJSqlParser.SingleStatement(CCJSqlParser.java:127)
at net.sf.jsqlparser.parser.CCJSqlParser.Statements(CCJSqlParser.java:458)
at net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatements(CCJSqlParserUtil.java:121)
... 1 more
Caused by:
net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "left" "LEFT"
at line 2, column 1.
Was expecting one of:
","
"SET"
at net.sf.jsqlparser.parser.CCJSqlParser.generateParseException(CCJSqlParser.java:20872)
at net.sf.jsqlparser.parser.CCJSqlParser.jj_consume_token(CCJSqlParser.java:20722)
at net.sf.jsqlparser.parser.CCJSqlParser.Update(CCJSqlParser.java:731)
at net.sf.jsqlparser.parser.CCJSqlParser.SingleStatement(CCJSqlParser.java:127)
at net.sf.jsqlparser.parser.CCJSqlParser.Statements(CCJSqlParser.java:458)
at net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatements(CCJSqlParserUtil.java:121)
原始SQL如下, 使用了多表连接更新:
update table1 t1
left join table1 t2 on t1.`order_id` = t2.`id`
set
t1.status =
#{status,jdbcType=VARCHAR}
where
t1.is_deleted = false
and t2.order_detail_id =
#{detailsId,jdbcType=BIGINT}
这两个表并不在多租户列表里,百度了一下这个异常发现有不少说是因为多组户插件引起的,明明做了表过滤,那是怎么影响到的呢。
问题排查
考虑到这个多表连接更新不常见,然后先试着改了下这个sql,使用子查询实现相同功能,如下
update table1 t1
set
t1.status =
#{status,jdbcType=VARCHAR}
where
t1.is_deleted = false and t1.`order_id` in (select id from t2 where order_detail_id =
#{detailsId,jdbcType=BIGINT})
随后发现使用此sql执行不会出现异常,看来只是影响了特定的sql,影响面还比较小,接下来继续分析原因。
找到关键的异常发生点, 异常类AbstractJsqlParser
的parser
方法(这个类是com.github.jsqlparser
包下的一个类,被Mybatis-plus所依赖):
CCJSqlParserUtil
这个类是个sql解析器, 通过parseStatements
方法(入参就是原始sql)来获取sql的
statment
数组, 这个statment
是用来干什么的呢,进入到下面的processParser
方法里面看:
这个statment
是用来判断sql的CURD类型,到这里大概知道这个多表连接更新的sql无法被方法net.sf.jsqlparser.parser.CCJSqlParserUtil#parseStatements
正常解析。但还有个问题,为啥我们的多租户表过滤没起作用呢,随后看到了这里(上面的图中,随便进入一个case里,找到实现类列表):
这个TenantSqlParser
就是最开始提到的依赖我们进行多组表过滤处理器ProjectHandler
的类,
对应的AbstractJsqlParser
的parse
方法是在com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler#sqlParser
这个方法中被调用:
这个类是咱们Mybatis-plus配置类PaginationInterceptor
的父类
到这里就知道为什么多租户表过滤没起作用了, 因为咱们一开始在
PaginationInterceptor
添加了TenantSqlParser
,而引入此配置之前PaginationInterceptor
的基类参数sqlParserList
为空, 不会执行sqlParser
的parse逻辑,也就不会异常,加了多租户插件之后,在执行多租户过滤前,AbstractSqlParserHandler
执行parse
方法提前抛出了sql解析异常,导致多租户过滤没起到作用。
问题解决
问题解决可参考官方github中的Release log
和issues
,地址贴在这里:
Release log: github.com/baomidou/my…
issue: github.com/baomidou/my…
方案一
在Mapper层接口方法上 加上注解:
@SqlParser(filter=true)
后续版本换成了:
@InterceptorIgnore(tenantLine = "true")
MP版本在3.1.1
版本以下的需要加入以下配置:
mybatis-plus:
global-config:
sql-parser-cache: true
考虑到项目中此类sql可能还有很多,需要一个可以覆盖所有此类sql的解决方案,因此方案一不是一个完美解决方案。
方案二
升级jsqlparser
,经过测试3.0
以后的net.sf.jsqlparser.parser.CCJSqlParserUtil#parseStatements
可以正常解析这种多表连接更新sql,
但是其他问题来了,MP本身的多组户插件类与3.0
版本的jssqlparser
不兼容,会报编译异常,
需要升级MP的版本,但不幸的是高版本的MP不兼容低版本的MP,这也是多数项目升级MP版本的痛点,升级的话需要更改很多之前用到MP的代码,方案二被放弃。
方案三
加入sqlParserFilter
, 对使用多租户解析器tenantSqlParser
增加过滤条件。
/**
* 自定义SqlParser
*
* @param properties 多租户配置
* @return SqlParser过滤器
*/
public ISqlParserFilter sqlParserFilter(ProjectTenantProperties properties) {
return metaObject -> {
Object boundSql = metaObject.getValue("boundSql");
String sql = String.valueOf(ReflectUtil
.getFieldValue(boundSql, "sql"));
//先对非多租户表进行一次过滤,非多组户表不进行sqlParser解析
return properties.getTables().stream().noneMatch(tableName -> StringUtils.containsIgnoreCase(sql, tableName));
};
}
对非租户表先进行过滤,也就是说之前的多租户处理器中的非租户表过滤逻辑可以被去掉了。
此过滤器在com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler#sqlParser
这个方法中被用到:
至此问题被解决完毕。
转载自:https://juejin.cn/post/7270900584466120758