当前使用版本(必填,否则不予处理)

mybatis-plus v3.5.1

该问题是如何引起的?(确定最新版也有问题再提!!!)

请参考原来的问题 https://github.com/baomidou/mybatis-plus/issues/4488 因为这个问题已经被设置为closed,所以重开一个问题

重现步骤(如果有就写完整)

首先感谢“miemieYaho”的快速回复,但针对回复的内容“association 配置的 select 查询是任何插件都不会走的”我们不太认可,因为我们恢复到mybatis-plus v3.2.0,然后把TenantLineInnerInterceptor改回旧版本的TenantSqlParser,对应的association中对应的子查询是可以正常添加TenantIdColumn "CID",如下恢复旧版的log所示:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53c2c402] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@75d7297d] will not be managed by Spring
Original SQL: SELECT  
USERID,SESSIONID,OPER_HOST,OPER_BROWSER,OPER_OS,LOGIN_DATE,LAST_OP_TIME
  FROM T_AT_SESSION 



  ORDER BY LOGIN_DATE DESC , USERID ASC
parser sql: SELECT USERID, SESSIONID, OPER_HOST, OPER_BROWSER, OPER_OS, LOGIN_DATE, LAST_OP_TIME FROM T_AT_SESSION WHERE T_AT_SESSION.CID = 1 ORDER BY LOGIN_DATE DESC, USERID ASC
==>  Preparing: SELECT USERID, SESSIONID, OPER_HOST, OPER_BROWSER, OPER_OS, LOGIN_DATE, LAST_OP_TIME FROM T_AT_SESSION WHERE T_AT_SESSION.CID=1 AND USERID IN(SELECT USERID FROM T_AT_SESSION WHERE T_AT_SESSION.CID = 1 ORDER BY LOGIN_DATE DESC, USERID ASC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY) ORDER BY LOGIN_DATE DESC, USERID ASC 
==> Parameters: 0(Long), 10(Long)
<==    Columns: USERID, SESSIONID, OPER_HOST, OPER_BROWSER, OPER_OS, LOGIN_DATE, LAST_OP_TIME
<==        Row: 68d1631efd519da694f3fe452b5d4242, 8097ff01-336a-453f-ba4a-f9d349b644bd, 127.0.0.1, Chrome 9, Windows 10, 2022-05-10 13:31:03, 2022-05-10 13:35:33.000949
Original SQL: SELECT * FROM T_USER WHERE USERID=? 
parser sql: SELECT * FROM T_USER WHERE USERID = ? AND T_USER.CID = 1
====>  Preparing: SELECT * FROM T_USER WHERE USERID = ? AND T_USER.CID = 1 
====> Parameters: 68d1631efd519da694f3fe452b5d4242(String)
<====    Columns: USERID, CID, NAME, LOGIN_NAME, SALT, PASSWORD, EMAIL, PHONE, MOBILE, FAX, ADDRESS, FAIL_TIMES, INIT_PWD, STATUS, LOCK_REASON, PWD_MODIFY_DATE, HISTORY_PWD, LAST_LOGIN_DATE, CREATOR_ID, CREATE_DATE, MODIFIER_ID, LAST_MODIFY_DATE, LAST_ROLE, VERSION
<====        Row: 68d1631efd519da694f3fe452b5d4242, 1, wing, wing, ac09f1adca4fcc26, 90dbe50a06cb254ef89bbc59d1276c119259ea438685f442949ca8e46a0b2a70, wing.li@lyodssoft.com, 110000, 120000000, 1300000, 1500, 0, 0, 0, null, 2021-10-20 11:53:49, ac09f1adca4fcc2690dbe50a06cb254ef89bbc59d1276c119259ea438685f442949ca8e46a0b2a70, 2022-05-10 13:31:03, 1, 2021-10-20 11:53:30, 1, 2021-12-03 16:28:14, 599571c6094435f813e5acb851a15357, 103
Original SQL: SELECT * FROM T_USER WHERE USERID=? 
parser sql: SELECT * FROM T_USER WHERE USERID = ? AND T_USER.CID = 1
======>  Preparing: SELECT * FROM T_USER WHERE USERID = ? AND T_USER.CID = 1 
======> Parameters: 1(String)
<======    Columns: USERID, CID, NAME, LOGIN_NAME, SALT, PASSWORD, EMAIL, PHONE, MOBILE, FAX, ADDRESS, FAIL_TIMES, INIT_PWD, STATUS, LOCK_REASON, PWD_MODIFY_DATE, HISTORY_PWD, LAST_LOGIN_DATE, CREATOR_ID, CREATE_DATE, MODIFIER_ID, LAST_MODIFY_DATE, LAST_ROLE, VERSION
<======        Row: 1, 1, administrator, admin, 247b29c80c634031, 5496d0d23e0d4900af586cd8edfdea67c9ff3b2feffbe42d6bc4df00527bb24f, null, null, null, null, null, 0, 0, 0, 1, 2021-06-15 18:47:02, 247b29c80c634031f4b75c3c1bbfb568b89e882240e46d6f388f64e1a3e1e8bd895716ef31510a8e, 2022-03-22 18:15:27, 1, 2013-10-10 12:00:00, 1, 2013-10-10 12:00:00, 1, 193
<======      Total: 1
<====      Total: 1
<==        Row: f0595d136a9657bd5610d29397a2ac22, dc80e9b7-5313-41c2-83a7-0027c2d1c7b7, 127.0.0.1, Chrome 55, Windows 10, 2022-05-10 13:27:49, 2022-05-10 13:28:35.000813

报错信息

从 v3.2.0的log我们可以看到如下信息: Original SQL: SELECT * FROM T_USER WHERE USERID=? parser sql: SELECT * FROM T_USER WHERE USERID = ? AND T_USER.CID = 1 即,证明旧版本3.2.0是可以支持association中对应的子查询可以正常添加TenantIdColumn "CID"

Comment From: kevinluoc

我们确认拦截器是肯定可以拦截到association中配置SQL的,因为我们继承的是mybatis中的org.apache.ibatis.plugin.Interceptor,我们自己尝试重写mybatis-plus中的MybatisPlusInterceptor.java方法“ intercept(Invocation invocation)”,增加“if (target instanceof Executor) {”中对应的else分支,是可以解决的,具体如下所示:

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
            Object parameter = args[1];
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
                    boundSql = (BoundSql) args[5];
                }
                System.err.println("select---<>:" + boundSql);
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, parameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, parameter);
                }
            }
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            // 目前只有StatementHandler.getBoundSql方法args才为null
            if (null == args) {
                for (InnerInterceptor innerInterceptor : interceptors) {
                    innerInterceptor.beforeGetBoundSql(sh);
                }
            } else {
                Connection connections = (Connection) args[0];
                Integer transactionTimeout = (Integer) args[1];
                for (InnerInterceptor innerInterceptor : interceptors) {
                    innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
                }
            }
            // 增加association的SQL處理
            BoundSql boundSql = sh.getBoundSql();
            PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
            MappedStatement ms = mpSh.mappedStatement();
            if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(null, ms, null, null, null, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(null, ms, null, null, null, boundSql);
                }
            }

        }
        return invocation.proceed();
    }

麻烦确认一下官方是否可以修改这一块内容,谢谢!

Comment From: huayanYu

欢迎 直接PR

Comment From: miemieYaho

InnerInterceptor 各个method都有对应的场景,你这种乱用法是不合适的

Comment From: kevinluoc

InnerInterceptor 各个method都有对应的场景,你这种乱用法是不合适的

谢谢,如果想要实现之前v3.2.0那种效果,需要如何实现比较合适,有没有什么建议?

Comment From: miemieYaho

根据test来看是能每次都进beforePrepare的,具体的你自己debug看看吧

Comment From: jimmy384

我也遇到这个问题,版本是3.4.3.4,贴一下我的解决方法

import cn.hutool.core.bean.BeanUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;

import java.sql.Statement;
import java.util.List;

@Intercepts(
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
)
public class NestedQueryInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof ResultSetHandler) {
            ResultSetHandler resultSetHandler = (ResultSetHandler) target;
            InnerObject innerObject = extractInnerObject(resultSetHandler);

            boolean hasNestedQuery = hasNestedQuery(innerObject.mappedStatement);
            if (hasNestedQuery) {
                /*
                 * Mybatis嵌套查询的流程
                 * 1.查询(Executor.query)
                 * |-2.封装结果(ResultSetHandler)
                 *   |-3.如果2中的resultMap定义了嵌套查询, 这里会再触发一次查询(Executor.query)
                 *
                 * 第1步中的executor是经过interceptor代理的对象
                 * 第3步中的executor是原生对象,没有经过interceptor代理
                 * 所以MybatisPlusInterceptor对嵌套查询不起作用,MybatisPlusInterceptor里面的DynamicTableNameInnerInterceptor自然也不起作用
                 *
                 * 现在想DynamicTableNameInnerInterceptor起作用, 这里把第3步的executor替换成经过interceptor代理的代理对象
                 */
                Object executor = pluginAll(innerObject.executor, innerObject.configuration.getInterceptors());
                BeanUtil.setProperty(target, "executor", executor);
            }
        }
        return target;
    }

    private boolean hasNestedQuery(MappedStatement mappedStatement) {
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
        boolean hasNestedQuery = false;
        if (CollectionUtils.isNotEmpty(resultMaps)) {
            for (ResultMap resultMap : resultMaps) {
                if (resultMap.hasNestedQueries()) {
                    hasNestedQuery = true;
                    break;
                }
            }
        }
        return hasNestedQuery;
    }

    private Object pluginAll(Object target, List<Interceptor> interceptors) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    private InnerObject extractInnerObject(ResultSetHandler resultSetHandler) {
        InnerObject innerObject = new InnerObject();
        innerObject.configuration = BeanUtil.getProperty(resultSetHandler, "configuration");
        innerObject.executor = BeanUtil.getProperty(resultSetHandler, "executor");
        innerObject.mappedStatement = BeanUtil.getProperty(resultSetHandler, "mappedStatement");
        return innerObject;
    }

    private static class InnerObject {
        Configuration configuration;
        Executor executor;
        MappedStatement mappedStatement;
    }

}

Comment From: Hccake

可以使用 h2 数据库提供一个最简的复现 demo

Comment From: qmdx

升级 3.5.3.2 后测试,还是未解决打开该问题反馈错误