• Mybatis插件-查看执行SQL


    前言

    SQL 的执行是通过 Statement 执行的,有的驱动 Statement 实现类有打印执行 SQL 的方法,而有的驱动没有,有打印 SQL 的方法直接执行就可以了,没有就只能手动拼接了。

    有打印 SQL 方法

    Mysql 的 Statement 有打印 SQL 的方法只需要获取 Statement 再执行对应的方法即可。MyBatis 的插件可以代理 ParameterHandlerResultSetHandlerStatementHandlerExecutor 4 个接口里面的方法,其中 StatementHandler 用于处理 Statement ,可以看到下面两个方法包含 Statement :

    int update(Statement statement)
        throws SQLException
    <E> List<E> query(Statement statement, ResultHandler resultHandler)
          throws SQLException;
    

    插件代码如下:

    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.executor.statement.StatementHandler;
    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.ResultHandler;
    import org.springframework.stereotype.Component;
    
    import java.sql.PreparedStatement;
    import java.sql.Statement;
    
    /**
     * @author haibara
     */
    @Component
    @Slf4j
    @Intercepts({
            @Signature(type = StatementHandler.class, method = "update", args = Statement.class),
            @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
    public class CustomInterceptor implements Interceptor {
      private final Class<?> clientPreparedStatement;
      public CustomInterceptor() throws ClassNotFoundException {
        this.clientPreparedStatement = Class.forName("com.mysql.cj.jdbc.ClientPreparedStatement");
      }
    
      @Override
      public Object intercept(Invocation invocation) throws Throwable {
        Object proceed = invocation.proceed();
        PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
        Object unwrap = ps.unwrap(clientPreparedStatement);
        String mysql = unwrap.toString();
        String sql = mysql.substring(mysql.indexOf(": ") + 1).trim();
        // 控制台打印替换占位符后的 SQL 语句
        System.out.println(!mysql.contains("EXCEPTION: ") ? sql : null);
        return proceed;
      }
    }
    

    没有打印 SQL 的方法

    没有打印 SQL 的方法就需要获取传给 Statement 的参数和 SQL 语句再手动拼接,获取步骤如下:

    1. 获取 StatementHandler(BaseStatementHandler) 中的 boundSql 。通过 boundSql.getSql() 获取包含有占位符的 SQL ,通过 boundSql.getParameterMappings() 获取参数。
    2. 通过 p6spy 转换参数值为对应在数据库当中的格式(自己实现需要将字符串类型替换 ` 为 `` ,还有时间和布尔等特殊类型转换)。
    3. 替换占位符。

    其中第一步参考 ParameterHandler 默认实现类 DefaultParameterHandlersetParameters 方法获取参数值,方法中用到的 5 个实例变量通过 Mybatis 的 MetaObject 反射获取。第二步替换占位符参考 p6spy 的 PreparedStatementInformationgetSqlWithValues ,p6spy 依赖:

    <dependency>
        <groupId>p6spy</groupId>
        <artifactId>p6spy</artifactId>
        <version>3.9.1</version>
    </dependency>
    

    插件代码如下:

    import com.p6spy.engine.common.Value;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.ParameterMapping;
    import org.apache.ibatis.mapping.ParameterMode;
    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.reflection.MetaObject;
    import org.apache.ibatis.reflection.SystemMetaObject;
    import org.apache.ibatis.session.Configuration;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.type.TypeHandlerRegistry;
    import org.springframework.stereotype.Component;
    
    import java.sql.Statement;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author haibara
     */
    @Component
    @Slf4j
    @Intercepts({
            @Signature(type = StatementHandler.class, method = "update", args = Statement.class),
            @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
    public class CustomInterceptor implements Interceptor {
    
      @Override
      public Object intercept(Invocation invocation) throws Throwable {
        Object proceed = invocation.proceed();
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 反射获取 DefaultParameterHandler setParameters 方法需要的实例变量
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        Configuration configuration = mappedStatement.getConfiguration();
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        BoundSql boundSql = statementHandler.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();
        // 获取准备预处理的 SQL 和用于替换占位符的参数值
        final String statementQuery = boundSql.getSql().replaceAll("\s+", " ");
        List<Value> parameterValues = new ArrayList<>();
        // 从 BoundSql 中获取参数值,用 p6spy 的 Value 包装参数值再保存到 parameterValues
        // 参考 org.apache.ibatis.scripting.defaults.DefaultParameterHandler 的 setParameters 方法
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          for (ParameterMapping parameterMapping : parameterMappings) {
            // 只获取输入的参数
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              String propertyName = parameterMapping.getProperty();
              if (boundSql.hasAdditionalParameter(propertyName)) {
                // 参数是 <foreach/> 或 <bind/> 标签中的
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                value = null;
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                // 参数与数据库字段直接映射
                value = parameterObject;
              } else {
                // 参数值需要通过反射从复杂对象中获取
                MetaObject mo = configuration.newMetaObject(parameterObject);
                value = mo.getValue(propertyName);
              }
              parameterValues.add(new Value(value));
            }
          }
        }
        // 替换 SQL 中的占位符。Java 类型 转为对应的 JdbcType 通过 p6spy Value 的 tosSring 方法
        // 参考 com.p6spy.engine.common.PreparedStatementInformation 的 getSqlWithValues 方法
        final StringBuilder sb = new StringBuilder();
        int currentParameter = 0;
        for (int pos = 0; pos < statementQuery.length(); pos++) {
          char character = statementQuery.charAt(pos);
          if (statementQuery.charAt(pos) == '?' && currentParameter < parameterValues.size()) {
            Value value = parameterValues.get(currentParameter);
            sb.append(value != null ? value.toString() : new Value().toString());
            currentParameter++;
          } else {
            sb.append(character);
          }
        }
        // 控制台打印替换占位符后的 SQL 语句
        System.out.println(sb.toString());
        return proceed;
      }
    }
    

    p6spy 是一个拦截数据库执行记录的框架,通过简单配置就可以打印 SQL,它的原理是代理数据源。不过有一处代码不太懂,PreparedStatementInformation#getSqlWithValues() 里是 currentParameter <= parameterValues.size(),为什么不是 currentParameter < parameterValues.size() 呢?

    自增列

    以 Mysql 数据库为例:

    • user 表结构:

      create table user
      (
          id   int auto_increment
              primary key,
          name varchar(20) not null
      );
      
    • Mybatis 对应 insert xml:

      <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.xxxx.User"
              useGeneratedKeys="true">
          insert into `user` (`name`)
          values (#{name,jdbcType=VARCHAR})
      </insert>
      

    在插件的最后添加下面代码:

    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyProperties = mappedStatement.getKeyProperties();
      String[] keyColumns = mappedStatement.getKeyColumns();
      MetaObject metaParam = configuration.newMetaObject(parameterObject);
      Object value = metaParam.getValue(keyProperties[0]);
      sb.insert(sb.indexOf("(") + 1, "`" + keyColumns[0] + "`, ");
      sb.insert(sb.indexOf("(", sb.indexOf(")")) + 1, value + ", ");
    }
    

    其他场景还是建议手动拼接 insert 语句。

    参考

    精尽MyBatis源码分析 - 插件机制 - 月圆吖 - 博客园 (cnblogs.com)

    Mybatis-PageHelper 分页插件实现

    精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下) - 月圆吖 - 博客园 (cnblogs.com)

    精尽MyBatis源码分析 - SQL执行过程(二)之 StatementHandler - 月圆吖 - 博客园 (cnblogs.com)

  • 相关阅读:
    cookie处理函数练习(为我所写,非我所想:改善面向对象)
    TypeScript的4种编译方式
    如何调用外部的Web API
    Json to JObject转换的使用方法
    Json.NET读取和写入Json文件
    XTemplate语法基础
    node.js xtemplate的使用实例
    node.js express安装及示例网站搭建
    各大互联网公司前端面试题(HTML/CSS)
    各大互联网公司前端面试题
  • 原文地址:https://www.cnblogs.com/hligy/p/15104461.html
Copyright © 2020-2023  润新知