Mybatis插件-查看执行SQL

Posted     装在瓶子里的西班牙阳光

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis插件-查看执行SQL相关的知识,希望对你有一定的参考价值。

前言

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

有打印 SQL 方法

mysqlStatement 有打印 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)

以上是关于Mybatis插件-查看执行SQL的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis-数据权限插件

MyBatis 插件 : 打印 SQL 及其执行时间

MyBatis 插件 : 打印 SQL 及其执行时间

MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间

MyBatis插件及示例----打印每条SQL语句及其执行时间

分页插件PageHelper配置步骤(mybatis)