likes
comments
collection
share

记Mybatis-plus多租户插件引起sql异常的问题解决过程

作者站长头像
站长
· 阅读数 65

前文

本篇大多数文本描述的是问题发生的始末及解决过程,想直接看解决方案的可以点击此处跳转。

问题背景

不久前收到一个需求,要求做两套一模一样的功能,这就要求进行数据隔离,通常的数据隔离方案有:

独立数据库
共享数据库,独立Schema(如Oracle、DB2等)
共享数据库,共享 Schema,共享数据表

因为我们使用的Mysql数据库,所以方案2首先被排除,使用方案1需引入多数据源组件,并新建数据库,因为这一个小功能分出两个库显然也有点儿大材小用,最终决定使用方案3,落地方案为引入Mybatis-plus多租户插件。

注: 已知目前项目依赖的mybatis-plus版本为3.1.2, jsqlParser版本为2.0

问题发生

因为我们是分布式架构,每个业务一个模块,而这个业务功能归属到一个已有模块业务,考虑到加入的多租户插件不能影响到同模块其他业务功能,因此进行了表过滤,多租户处理器相关代码如下:

记Mybatis-plus多租户插件引起sql异常的问题解决过程

然后Mybatis-plus配置里加入此处理器:

记Mybatis-plus多租户插件引起sql异常的问题解决过程

标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,影响面还比较小,接下来继续分析原因。

找到关键的异常发生点, 异常类AbstractJsqlParserparser方法(这个类是com.github.jsqlparser包下的一个类,被Mybatis-plus所依赖):

记Mybatis-plus多租户插件引起sql异常的问题解决过程 CCJSqlParserUtil这个类是个sql解析器, 通过parseStatements方法(入参就是原始sql)来获取sql的 statment数组, 这个statment是用来干什么的呢,进入到下面的processParser方法里面看:

记Mybatis-plus多租户插件引起sql异常的问题解决过程

这个statment是用来判断sql的CURD类型,到这里大概知道这个多表连接更新的sql无法被方法net.sf.jsqlparser.parser.CCJSqlParserUtil#parseStatements正常解析。但还有个问题,为啥我们的多租户表过滤没起作用呢,随后看到了这里(上面的图中,随便进入一个case里,找到实现类列表):

记Mybatis-plus多租户插件引起sql异常的问题解决过程

这个TenantSqlParser就是最开始提到的依赖我们进行多组表过滤处理器ProjectHandler的类, 对应的AbstractJsqlParserparse方法是在com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler#sqlParser这个方法中被调用:

记Mybatis-plus多租户插件引起sql异常的问题解决过程

这个类是咱们Mybatis-plus配置类PaginationInterceptor的父类

记Mybatis-plus多租户插件引起sql异常的问题解决过程 到这里就知道为什么多租户表过滤没起作用了, 因为咱们一开始在PaginationInterceptor添加了TenantSqlParser,而引入此配置之前PaginationInterceptor的基类参数sqlParserList为空, 不会执行sqlParser的parse逻辑,也就不会异常,加了多租户插件之后,在执行多租户过滤前,AbstractSqlParserHandler执行parse方法提前抛出了sql解析异常,导致多租户过滤没起到作用。

问题解决

问题解决可参考官方github中的Release logissues,地址贴在这里:

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不兼容,会报编译异常,

记Mybatis-plus多租户插件引起sql异常的问题解决过程

需要升级MP的版本,但不幸的是高版本的MP不兼容低版本的MP,这也是多数项目升级MP版本的痛点,升级的话需要更改很多之前用到MP的代码,方案二被放弃。

方案三

加入sqlParserFilter, 对使用多租户解析器tenantSqlParser增加过滤条件。

记Mybatis-plus多租户插件引起sql异常的问题解决过程

/**  
* 自定义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这个方法中被用到:

记Mybatis-plus多租户插件引起sql异常的问题解决过程 至此问题被解决完毕。

转载自:https://juejin.cn/post/7270900584466120758
评论
请登录