MyBatis-Plus批量操作SQL日志不打印问题的解决方案

 更新时间:2026年03月05日 08:57:09   作者:程序员食堂  
在使用 MyBatis-Plus 的 saveBatch() 和 updateBatchById() 方法进行批量数据操作时,发现自定义的 Druid SQL 日志拦截器无法打印这些批量操作的 SQL 语句,导致调试和问题排查困难,本文给大家该问题的详细解决方案,需要的朋友可以参考下

问题描述

在使用 MyBatis-Plus 的 saveBatch() 和 updateBatchById() 方法进行批量数据操作时,发现自定义的 Druid SQL 日志拦截器(SqlLogInterceptor)无法打印这些批量操作的 SQL 语句,导致调试和问题排查困难。

问题分析

1. 批量操作的特殊性

MyBatis-Plus 的批量操作方法(如 saveBatchupdateBatchById)使用了 JDBC 的批处理(Batch)模式来提高性能。批处理模式与普通的单条 SQL 执行方式不同:

  • 普通执行:每条 SQL 单独执行,会触发 statementExecuteAfterstatementExecuteUpdateAfter 等方法
  • 批处理执行:多条 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

注意事项

  1. 性能考虑:DEBUG 级别的 MyBatis 日志会输出大量信息,生产环境建议关闭或设置为 INFO 级别
  2. 日志过滤:可以在 SqlLogInterceptor 中添加过滤逻辑,避免打印某些不需要的 SQL(如健康检查)
  3. 批量大小:MyBatis-Plus 默认批量大小为 1000,可以通过配置调整
  4. 事务管理:批量操作需要在事务中执行,确保添加 @Transactional 注解

总结

MyBatis-Plus 批量操作 SQL 不打印的问题主要是因为批处理模式使用了不同的执行路径,原有的拦截器没有处理 statementExecuteBatchAfter 方法。通过修改拦截器并配合 MyBatis 原生日志,可以完美解决这个问题,既能保证日志的完整性,又能在需要时查看详细的执行信息。

以上就是MyBatis-Plus批量操作SQL日志不打印问题的解决方案的详细内容,更多关于MyBatis-Plus操作SQL日志不打印的资料请关注脚本之家其它相关文章!

相关文章

  • Java职责链模式的深入了解

    Java职责链模式的深入了解

    这篇文章主要为大家介绍了Java职责链模式,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • Java之Spring简单的读取和存储对象

    Java之Spring简单的读取和存储对象

    这篇文章主要介绍了Spring的读取和存储对象,获取 bean 对象也叫做对象装配,是把对象取出来放到某个类中,有时候也叫对象注⼊,想进一步了解的同学可以参考本文
    2023-04-04
  • java zxing合成复杂二维码图片示例详解

    java zxing合成复杂二维码图片示例详解

    这篇文章主要为大家介绍了java zxing合成复杂二维码图片示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • Spring实战之使用静态工厂方法创建Bean操作示例

    Spring实战之使用静态工厂方法创建Bean操作示例

    这篇文章主要介绍了Spring实战之使用静态工厂方法创建Bean操作,结合实例形式分析了静态工厂方法创建Bean的相关实现步骤与操作注意事项,需要的朋友可以参考下
    2019-11-11
  • SpringBoot自定义Starter的教程指南

    SpringBoot自定义Starter的教程指南

    SpringBoot的Starter自动配置机制极大地简化了依赖管理和应用配置,使得开发者可以以最少的配置快速启动和运行Spring应用,有时,标准的Starter可能无法满足特定需求,需要创建自定义Starter,所以本文给大家介绍了SpringBoot自定义Starter的教程指南
    2024-11-11
  • Java实现加盐算法的两种方法

    Java实现加盐算法的两种方法

    数据安全是一个重要的问题,本文主要介绍了Java实现加盐算法的两种方法,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • win11 idea shift+F6快捷键失效问题解决方案

    win11 idea shift+F6快捷键失效问题解决方案

    这篇文章主要介绍了win11 idea shift+F6快捷键失效问题,本文给大家分享最新解决方案,需要的朋友可以参考下
    2023-08-08
  • 创建Java项目时常见的三种构建系统详解

    创建Java项目时常见的三种构建系统详解

    这篇文章主要介绍了创建Java项目时常见的三种构建系统,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05
  • Spring websocket并发发送消息异常的解决

    Spring websocket并发发送消息异常的解决

    本文主要介绍了 Spring websocket并发发送消息异常的解决,当多个线程同时尝试通过 WebSocket 会话发送消息时,会抛出异常,下面就来解决一下,感兴趣的可以了解一下
    2023-09-09
  • idea左下角的Git(Version Control)中显示Local Changes窗口方式

    idea左下角的Git(Version Control)中显示Local Changes窗口方式

    在IDEA中,通过使用快捷键Alt+9(Windows)或Cmd+9(Mac)可以快速打开LocalChanges窗口,查看当前Git仓库的本地变更,若此方法不可用,可尝试进入settings,点击VersionControl,选择Commit,并取消Use interface的勾选
    2024-10-10

最新评论