MyBatis-Plus批量操作SQL日志不打印問(wèn)題的解決方案
問(wèn)題描述
在使用 MyBatis-Plus 的 saveBatch() 和 updateBatchById() 方法進(jìn)行批量數(shù)據(jù)操作時(shí),發(fā)現(xiàn)自定義的 Druid SQL 日志攔截器(SqlLogInterceptor)無(wú)法打印這些批量操作的 SQL 語(yǔ)句,導(dǎo)致調(diào)試和問(wèn)題排查困難。
問(wèn)題分析
1. 批量操作的特殊性
MyBatis-Plus 的批量操作方法(如 saveBatch、updateBatchById)使用了 JDBC 的批處理(Batch)模式來(lái)提高性能。批處理模式與普通的單條 SQL 執(zhí)行方式不同:
- 普通執(zhí)行:每條 SQL 單獨(dú)執(zhí)行,會(huì)觸發(fā)
statementExecuteAfter、statementExecuteUpdateAfter等方法 - 批處理執(zhí)行:多條 SQL 一起提交執(zhí)行,只會(huì)觸發(fā)
statementExecuteBatchAfter方法
2. 原有攔截器的問(wèn)題
原有的 SqlLogInterceptor 繼承自 Druid 的 FilterEventAdapter,實(shí)現(xiàn)了以下方法:
@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();
// 這里沒(méi)有打印日志!
}可以看到,statementExecuteBatchAfter 方法中只設(shè)置了執(zhí)行時(shí)間,但沒(méi)有打印 SQL 日志,這就是批量操作 SQL 不打印的根本原因。
解決方案
方案一:修改 Druid 攔截器(推薦)
修改 SqlLogInterceptor.java 的 statementExecuteBatchAfter 方法,添加 SQL 日志打印邏輯:
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
// 批量執(zhí)行后也打印SQL日志
String sql = statement.getBatchSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
} else {
// 如果批量SQL為空,嘗試獲取最后執(zhí)行的SQL
sql = statement.getLastExecuteSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
}
}
}優(yōu)點(diǎn):
- 統(tǒng)一的日志格式
- 包含執(zhí)行時(shí)間統(tǒng)計(jì)
- 可以格式化 SQL 和參數(shù)
缺點(diǎn):
- 批量操作可能只顯示一條 SQL 模板,看不到每條具體的參數(shù)
方案二:?jiǎn)⒂?MyBatis 原生日志
在 logback.xml 配置文件中添加 MyBatis Mapper 的 DEBUG 級(jí)別日志:
<!-- MyBatis SQL 日志 --> <logger name="com.hzys.mapper" level="DEBUG"/>
優(yōu)點(diǎn):
- 可以看到每條 SQL 的詳細(xì)參數(shù)
- MyBatis 原生支持,穩(wěn)定可靠
缺點(diǎn):
- 日志格式與自定義攔截器不一致
- 日志量較大
方案三:雙管齊下(最佳實(shí)踐)
同時(shí)使用方案一和方案二,既能保證批量操作的 SQL 被記錄,又能在需要時(shí)查看詳細(xì)的參數(shù)信息。
完整代碼示例
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();
// 批量執(zhí)行后也打印SQL日志
String sql = statement.getBatchSql();
if (StringUtil.isNotBlank(sql)) {
printSqlLog(statement, sql);
} else {
// 如果批量SQL為空,嘗試獲取最后執(zhí)行的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 {
// 獲取參數(shù)
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">
<!-- 其他配置... -->
<!-- 日志輸出級(jí)別 -->
<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;
}
}驗(yàn)證效果
修改后,執(zhí)行批量操作時(shí)會(huì)看到類(lèi)似以下的日志輸出:
============== 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), 高級(jí)工程師(String), ... ==> Parameters: 2(Long), 李四(String), 中級(jí)工程師(String), ... <== Updates: 2
注意事項(xiàng)
- 性能考慮:DEBUG 級(jí)別的 MyBatis 日志會(huì)輸出大量信息,生產(chǎn)環(huán)境建議關(guān)閉或設(shè)置為 INFO 級(jí)別
- 日志過(guò)濾:可以在
SqlLogInterceptor中添加過(guò)濾邏輯,避免打印某些不需要的 SQL(如健康檢查) - 批量大小:MyBatis-Plus 默認(rèn)批量大小為 1000,可以通過(guò)配置調(diào)整
- 事務(wù)管理:批量操作需要在事務(wù)中執(zhí)行,確保添加
@Transactional注解
總結(jié)
MyBatis-Plus 批量操作 SQL 不打印的問(wèn)題主要是因?yàn)榕幚砟J绞褂昧瞬煌膱?zhí)行路徑,原有的攔截器沒(méi)有處理 statementExecuteBatchAfter 方法。通過(guò)修改攔截器并配合 MyBatis 原生日志,可以完美解決這個(gè)問(wèn)題,既能保證日志的完整性,又能在需要時(shí)查看詳細(xì)的執(zhí)行信息。
以上就是MyBatis-Plus批量操作SQL日志不打印問(wèn)題的解決方案的詳細(xì)內(nèi)容,更多關(guān)于MyBatis-Plus操作SQL日志不打印的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java之Spring簡(jiǎn)單的讀取和存儲(chǔ)對(duì)象
這篇文章主要介紹了Spring的讀取和存儲(chǔ)對(duì)象,獲取 bean 對(duì)象也叫做對(duì)象裝配,是把對(duì)象取出來(lái)放到某個(gè)類(lèi)中,有時(shí)候也叫對(duì)象注?,想進(jìn)一步了解的同學(xué)可以參考本文2023-04-04
Spring實(shí)戰(zhàn)之使用靜態(tài)工廠方法創(chuàng)建Bean操作示例
這篇文章主要介紹了Spring實(shí)戰(zhàn)之使用靜態(tài)工廠方法創(chuàng)建Bean操作,結(jié)合實(shí)例形式分析了靜態(tài)工廠方法創(chuàng)建Bean的相關(guān)實(shí)現(xiàn)步驟與操作注意事項(xiàng),需要的朋友可以參考下2019-11-11
win11?idea?shift+F6快捷鍵失效問(wèn)題解決方案
這篇文章主要介紹了win11?idea?shift+F6快捷鍵失效問(wèn)題,本文給大家分享最新解決方案,需要的朋友可以參考下2023-08-08
創(chuàng)建Java項(xiàng)目時(shí)常見(jiàn)的三種構(gòu)建系統(tǒng)詳解
這篇文章主要介紹了創(chuàng)建Java項(xiàng)目時(shí)常見(jiàn)的三種構(gòu)建系統(tǒng),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-05-05
Spring websocket并發(fā)發(fā)送消息異常的解決
本文主要介紹了 Spring websocket并發(fā)發(fā)送消息異常的解決,當(dāng)多個(gè)線程同時(shí)嘗試通過(guò) WebSocket 會(huì)話(huà)發(fā)送消息時(shí),會(huì)拋出異常,下面就來(lái)解決一下,感興趣的可以了解一下2023-09-09
idea左下角的Git(Version Control)中顯示Local Changes窗口方式
在IDEA中,通過(guò)使用快捷鍵Alt+9(Windows)或Cmd+9(Mac)可以快速打開(kāi)LocalChanges窗口,查看當(dāng)前Git倉(cāng)庫(kù)的本地變更,若此方法不可用,可嘗試進(jìn)入settings,點(diǎn)擊VersionControl,選擇Commit,并取消Use interface的勾選2024-10-10

