Mybatis 插件兼容动态SQL

系列

  • MyBatis拦截器原理介绍
  • Mybatis拦截器改写请求参数和结果
  • Mybatis 插件兼容动态SQL
  • mybatis 参数解析流程附相关案例
  • 再谈mybatis执行流程

开篇

  • 在mybatis的语法当中支持一种动态SQL的语法,比较常见的是条件判断,这类语句会结合实际传入的参数拼接完整的待执行的SQL 语句。

  • 在某种场景下我们会需要在 mybatis的拦截器中动态修改入参并期望生成新的SQL语句。

  • 一般场景下上述的情况应该在 Executor 的拦截插件中,但是事实上可以在ParameterHandler或StatementHandler 插件当中进行调整。

  • 建议通过实际实践可以发现真正的逻辑,如果能够能完整的调试会更加印象深刻。

例子


  INSERT INTO user
  
    
      pkid,
    
    
      `name`,
    
    
      password,
    
    
      phone
    
  
  
    
        #{pkId},
    
    
        #{userName},
    
    
      #{password},
    
    
      #{phone},
    
  

  • 上述SQL当中我们通过修改入参对象的值来动态修改生成的INSERT语句,如修改 phone字段为 null,那么整个INSERT语句就不包含phone字段。

ParameterHandler拦截

image.png
  • mybatis真正执行的SQL对象是 BoundSql 对象,而 BoundSql 语句是通过MappedStatement来返回的。
  • MappedStatement.getBoundSql是通过sqlSource.getBoundSql 语句来生成 BoundSql对象。
  • 通过动态修改 SqlSource 对象 或 修改MappedStatement来达到目的
@Intercepts({
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)
})
public class ParamInterceptor implements Interceptor {

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // 获取拦截器拦截的设置参数对象DefaultParameterHandler
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();

        // 通过mybatis的反射来获取对应的值
        MetaObject metaParameterHandler = MetaObject.forObject(parameterHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
        MappedStatement mappedStatement = (MappedStatement) metaParameterHandler.getValue("mappedStatement");
        Object parameterObject = metaParameterHandler.getValue("parameterObject");

        // id字段对应执行的SQL的方法的全路径,包含类名和方法名
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);

        // 动态加载类并获取类中的方法
        final Method[] methods = Class.forName(className).getMethods();

        // 遍历类的所有方法并找到此次调用的方法
        for (Method method : methods) {
            if (method.getName().equalsIgnoreCase(methodName)
                    && method.isAnnotationPresent(ParamAnnotation.class)) {

                // 获取方法上的注解以及注解对应的参数
                ParamAnnotation paramAnnotation = method.getAnnotation(ParamAnnotation.class);
                String srcKey = paramAnnotation.srcKey()[0];
                String destKey = paramAnnotation.destKey()[0];

                // 反射获取参数对象
                MetaObject param = MetaObject.forObject(parameterObject, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
                Object srcValue = param.getValue(srcKey);

                // 动态加工指定参数
                String destValue = String.valueOf(srcValue) + "fix";

                // 将修改后的动态参数添加到请求参数当中
                param.setValue(destKey, destValue);
                param.setValue(destKey, null);
                break;
            }
        }

        // 修改入参后动态生成的BoundSql对象
        BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
        String newSql = boundSql.getSql();

        // 生成新的BoundSql
        BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), newSql,
                boundSql.getParameterMappings(), boundSql.getParameterObject());

        // 1、通过生成新的MappedStatement对象
        MappedStatement newMappedStatement = newMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
        for (ParameterMapping mapping : boundSql.getParameterMappings()) {
            String prop = mapping.getProperty();
            if (boundSql.hasAdditionalParameter(prop)) {
                newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
            }
        }
        metaParameterHandler.setValue("mappedStatement", newMappedStatement);

        // 2、通过生成新的SqlSource对象
        MetaObject metaMappedStatement = MetaObject.forObject(mappedStatement, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
        metaMappedStatement.setValue("sqlSource", new BoundSqlSqlSource(newBoundSql));

        // 回写parameterObject对象
        metaParameterHandler.setValue("parameterObject", parameterObject);
        return invocation.proceed();
    }


    class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;
        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }

    }

    private MappedStatement newMappedStatement (MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new
                MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());

        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }

        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
  • 1、通过BoundSql boundSql = mappedStatement.getBoundSql(parameterObject)来获取新的BoundSql对象,进而获取文本的SQL语句。
  • 2、通过BoundSql的构造函数构建新的BoundSql 对象。
  • 3、BoundSqlSqlSource继承SqlSource来生成 SqlSource 对象。
  • 4、通过newMappedStatement生成新的MappedStatement对象。
  • 步骤3和步骤4只要选择其中一个就可以了,需要实际实践一下。

参考

  • Mybatis拦截ParameterHandler修改SQL
  • SpringBoot通过自定义Mybatis拦截器,实现SQL的改写

你可能感兴趣的:(Mybatis 插件兼容动态SQL)