MyBatis-Plus批量操作SQL日志不打印问题的解决方案
作者:程序员食堂
问题描述
在使用 MyBatis-Plus 的 saveBatch() 和 updateBatchById() 方法进行批量数据操作时,发现自定义的 Druid SQL 日志拦截器(SqlLogInterceptor)无法打印这些批量操作的 SQL 语句,导致调试和问题排查困难。
问题分析
1. 批量操作的特殊性
MyBatis-Plus 的批量操作方法(如 saveBatch、updateBatchById)使用了 JDBC 的批处理(Batch)模式来提高性能。批处理模式与普通的单条 SQL 执行方式不同:
- 普通执行:每条 SQL 单独执行,会触发
statementExecuteAfter、statementExecuteUpdateAfter等方法 - 批处理执行:多条 SQL 一起提交执行,只会触发
statementExecuteBatchAfter方法
2. 原有拦截器的问题
原有的 SqlLogInterceptor 继承自 Druid 的 FilterEventAdapter,实现了以下方法:
@Override
protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
statement.setLastExecuteTimeNano();
printSqlLog(statement, sql);
}
@Override
protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
statement.setLastExecuteTimeNano();
printSqlLog(statement, sql);
}
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
// 这里没有打印日志!
}可以看到,statementExecuteBatchAfter 方法中只设置了执行时间,但没有打印 SQL 日志,这就是批量操作 SQL 不打印的根本原因。
解决方案
方案一:修改 Druid 拦截器(推荐)
修改 SqlLogInterceptor.java 的 statementExecuteBatchAfter 方法,添加 SQL 日志打印逻辑:
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
// 批量执行后也打印SQL日志
String sql = statement.getBatchSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
} else {
// 如果批量SQL为空,尝试获取最后执行的SQL
sql = statement.getLastExecuteSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
}
}
}优点:
- 统一的日志格式
- 包含执行时间统计
- 可以格式化 SQL 和参数
缺点:
- 批量操作可能只显示一条 SQL 模板,看不到每条具体的参数
方案二:启用 MyBatis 原生日志
在 logback.xml 配置文件中添加 MyBatis Mapper 的 DEBUG 级别日志:
<!-- MyBatis SQL 日志 --> <logger name="com.hzys.mapper" level="DEBUG"/>
优点:
- 可以看到每条 SQL 的详细参数
- MyBatis 原生支持,稳定可靠
缺点:
- 日志格式与自定义拦截器不一致
- 日志量较大
方案三:双管齐下(最佳实践)
同时使用方案一和方案二,既能保证批量操作的 SQL 被记录,又能在需要时查看详细的参数信息。
完整代码示例
1. 修改后的 SqlLogInterceptor
@Slf4j
public class SqlLogInterceptor extends FilterEventAdapter {
private static final SQLUtils.FormatOption FORMAT_OPTION = new SQLUtils.FormatOption(false, false);
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
// 批量执行后也打印SQL日志
String sql = statement.getBatchSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
} else {
// 如果批量SQL为空,尝试获取最后执行的SQL
sql = statement.getLastExecuteSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
}
}
}
private void printSqlLog(StatementProxy statement, String sql) {
if (!log.isInfoEnabled() || StringUtil.isEmpty(sql)) {
return;
}
try {
// 获取参数
int parametersSize = statement.getParametersSize();
List<Object> parameters = new ArrayList<>(parametersSize);
for (int i = 0; i < parametersSize; ++i) {
parameters.add(getJdbcParameter(statement.getParameter(i)));
}
// 格式化SQL
String dbType = statement.getConnectionProxy().getDirectDataSource().getDbType();
String formattedSql = SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION);
// 打印日志
printSql(formattedSql, statement);
} catch (Exception e) {
log.error("SQL 格式化失败", e);
log.info("\n\n============== Sql Start ==============\n" +
"Execute SQL : {}\n" +
"Execute Time: {}\n" +
"============== Sql End ==============\n",
sql, StringUtil.format(statement.getLastExecuteTimeNano()));
}
}
private static void printSql(String sql, StatementProxy statement) {
String sqlLogger = "\n\n============== Sql Start ==============" +
"\nExecute SQL : {}" +
"\nExecute Time: {}" +
"\n============== Sql End ==============\n";
log.info(sqlLogger, sql.trim(), StringUtil.format(statement.getLastExecuteTimeNano()));
}
}2. Logback 配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<!-- 其他配置... -->
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="INFO"/>
<appender-ref ref="WARN"/>
<appender-ref ref="ERROR"/>
</root>
<!-- MyBatis SQL 日志 -->
<logger name="com.hzys.mapper" level="DEBUG"/>
<logger name="net.sf.ehcache" level="INFO"/>
<logger name="druid.sql" level="INFO"/>
</configuration>3. Druid 配置
@Slf4j
@Configuration
public class DruidConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.druid")
public DataSource dataSource() {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
// 添加自定义的SQL日志拦截器
SqlLogInterceptor sqlLogInterceptor = new SqlLogInterceptor();
dataSource.getProxyFilters().add(sqlLogInterceptor);
return dataSource;
}
}验证效果
修改后,执行批量操作时会看到类似以下的日志输出:
============== Sql Start ============== Execute SQL : INSERT INTO employee_hourly_rate (id, project_member, member_level, ...) VALUES (?, ?, ?, ...) Execute Time: 15ms ============== Sql End ============== ==> Preparing: INSERT INTO employee_hourly_rate (id, project_member, member_level, ...) VALUES (?, ?, ?, ...) ==> Parameters: 1(Long), 张三(String), 高级工程师(String), ... ==> Parameters: 2(Long), 李四(String), 中级工程师(String), ... <== Updates: 2
注意事项
- 性能考虑:DEBUG 级别的 MyBatis 日志会输出大量信息,生产环境建议关闭或设置为 INFO 级别
- 日志过滤:可以在
SqlLogInterceptor中添加过滤逻辑,避免打印某些不需要的 SQL(如健康检查) - 批量大小:MyBatis-Plus 默认批量大小为 1000,可以通过配置调整
- 事务管理:批量操作需要在事务中执行,确保添加
@Transactional注解
总结
MyBatis-Plus 批量操作 SQL 不打印的问题主要是因为批处理模式使用了不同的执行路径,原有的拦截器没有处理 statementExecuteBatchAfter 方法。通过修改拦截器并配合 MyBatis 原生日志,可以完美解决这个问题,既能保证日志的完整性,又能在需要时查看详细的执行信息。
以上就是MyBatis-Plus批量操作SQL日志不打印问题的解决方案的详细内容,更多关于MyBatis-Plus操作SQL日志不打印的资料请关注脚本之家其它相关文章!
