Mybatis插件-查看执行SQL
Posted 装在瓶子里的西班牙阳光
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis插件-查看执行SQL相关的知识,希望对你有一定的参考价值。
前言
SQL 的执行是通过 Statement 执行的,有的驱动 Statement 实现类有打印执行 SQL 的方法,而有的驱动没有,有打印 SQL 的方法直接执行就可以了,没有就只能手动拼接了。
有打印 SQL 方法
mysql 的 Statement
有打印 SQL 的方法只需要获取 Statement 再执行对应的方法即可。MyBatis 的插件可以代理 ParameterHandler
、 ResultSetHandler
、 StatementHandler
和 Executor
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 语句再手动拼接,获取步骤如下:
- 获取
StatementHandler(BaseStatementHandler)
中的boundSql
。通过boundSql.getSql()
获取包含有占位符的 SQL ,通过boundSql.getParameterMappings()
获取参数。 - 通过 p6spy 转换参数值为对应在数据库当中的格式(自己实现需要将字符串类型替换 ` 为 `` ,还有时间和布尔等特殊类型转换)。
- 替换占位符。
其中第一步参考 ParameterHandler
默认实现类 DefaultParameterHandler
的 setParameters
方法获取参数值,方法中用到的 5 个实例变量通过 Mybatis 的 MetaObject
反射获取。第二步替换占位符参考 p6spy 的 PreparedStatementInformation
的 getSqlWithValues
,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源码分析 - MyBatis初始化(四)之 SQL 初始化(下) - 月圆吖 - 博客园 (cnblogs.com)
精尽MyBatis源码分析 - SQL执行过程(二)之 StatementHandler - 月圆吖 - 博客园 (cnblogs.com)
以上是关于Mybatis插件-查看执行SQL的主要内容,如果未能解决你的问题,请参考以下文章
MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间