1. 程式人生 > 其它 >Mybatis外掛-檢視執行SQL

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)