以下操作基于 JDK1.8 ,以及 JDBC 5.1.48 完成。
在 MySQL JDBC 中,批操作的提交默认是逐条进行的。而在链接中加入以下参数,会把多条语句合并成一条提交。当 SQL 语句累积到一定数量(由数据库可接受的最大数据包大小决定),再一次性提交到数据库,减少了与数据库的交互次数,大大提高了效率。
rewriteBatchedStatements = true
下面说一下,在使用 rewriteBatchedStatements 的时候,需要注意的事项。
下面有请源码:
protected long[] executeBatchInternal() throws SQLException {
...
nbrCommands = (long[])this.batchedArgs.size(); // 获取批操作语句数量
this.batchedGeneratedKeys = new ArrayList(this.batchedArgs.size());
boolean multiQueriesEnabled = locallyScopedConn.getAllowMultiQueries(); // 当前连接是否允许一次查询多条语句,默认为 false
Object sqlEx;
if (locallyScopedConn.versionMeetsMinimum(4, 1, 1) && (multiQueriesEnabled || locallyScopedConn.getRewriteBatchedStatements() && nbrCommands > 4)) {
sqlEx = this.executeBatchUsingMultiQueries(multiQueriesEnabled, (int)nbrCommands, individualStatementTimeout);
return (long[])sqlEx;
}
...
}
我们可以看到,在 Statement 中,批操作语句总数大于 4 条时(nbrCommands > 4),才会合并成一条语句提交。
public int[] executeBatch() throws SQLException {
...
if (this.connection.versionMeetsMinimum(4, 1, 0) && !this.batchHasPlainStatements && this.batchedArgs != null && this.batchedArgs.size() > 3) {
var3 = this.executePreparedBatchAsMultiStatement(batchTimeout);
return var3;
}
...
}
而在 PreparedStatement 中,批操作语句总数大于 3 条时(this.batchedArgs.size() > 3),才会合并成一条语句提交。
在实际中,我们会遇到自己或同事写的 SQL 语句末尾有一个分号的情况,用起来也没有什么问题。但在链接加上 rewriteBatchedStatements=true ,则会在运行时报错。
为什么会这样呢?下面有请源码:
private int[] executeBatchUsingMultiQueries(boolean multiQueriesEnabled, int nbrCommands, int individualStatementTimeout) throws SQLException {
...
for(commandIndex = 0; commandIndex < nbrCommands; ++commandIndex) {
...
queryBuf.append(nextQuery);
queryBuf.append(";");
++argumentSetsInBatchSoFar;
}
...
}
protected int[] executePreparedBatchAsMultiStatement(int batchTimeout) throws SQLException {
synchronized(this.checkClosed().getConnectionMutex()) {
if (this.batchedValuesClause == null) {
this.batchedValuesClause = this.originalSql + ";"; //在 SQL 语句末尾加上分号
}
...
}
}
我们可以看到,对于批操作,JDBC 会在 SQL 语句的末尾加上分号。
batchedValuesClause 默认为 null,由于能力有限,目前仍找不到修改的办法,找到后会更新上来。
有时,我们会用到 executeBatch 的返回值,该方法会返回一个 int 数组,其中,第 n 项代表着第 n 条语句是否执行成功(0,1)。
但在使用 rewriteBatchedStatements 后,对于 PreparedStatement 的批量插入操作来说,它返回的每一项代表着整个批操作的总成功数。
下面再次有请源码:
protected int[] executePreparedBatchAsMultiStatement(int batchTimeout) throws SQLException {
...
if (!this.batchHasPlainStatements && this.connection.getRewriteBatchedStatements()) {
if (this.canRewriteAsMultiValueInsertAtSqlLevel()) {
var3 = this.executeBatchedInserts(batchTimeout);
return var3;
}
...
}
public boolean canRewriteAsMultiValueInsertAtSqlLevel() throws SQLException {
return this.parseInfo.canRewriteAsMultiValueInsert;
}
我们可以看到,在使用 rewriteBatchedStatements 后,JDBC 会先调用 canRewriteAsMultiValueInsertAtSqlLevel() 方法,判断是否允许将返回结果重写。
而该方法返回的是 ParseInfo 里的 canRewriteAsMultiValueInsert 属性。下面是该属性的赋值代码:
public ParseInfo(String sql, MySQLConnection conn, DatabaseMetaData dbmd, String encoding, SingleByteCharsetConverter converter, boolean buildRewriteInfo) throws SQLException {
...
if (buildRewriteInfo) { // 是否重写Info
// 是否运行重写批量插入语句的结果
this.canRewriteAsMultiValueInsert = PreparedStatement.canRewrite(sql, this.isOnDuplicateKeyUpdate, this.locationOfOnDuplicateKeyUpdate, this.statementStartPos) && !this.parametersInDuplicateKeyClause;
if (this.canRewriteAsMultiValueInsert && conn.getRewriteBatchedStatements()) {
this.buildRewriteBatchedParams(sql, conn, dbmd, encoding, converter);
}
...
}
protected static boolean canRewrite(String sql, boolean isOnDuplicateKeyUpdate, int locationOfOnDuplicateKeyUpdate, int statementStartPos) {
boolean rewritableOdku = true;
if (isOnDuplicateKeyUpdate) {
int updateClausePos = StringUtils.indexOfIgnoreCase(locationOfOnDuplicateKeyUpdate, sql, " UPDATE ");
if (updateClausePos != -1) {
rewritableOdku = StringUtils.indexOfIgnoreCaseRespectMarker(updateClausePos, sql, "LAST_INSERT_ID", "\"'`", "\"'`", false) == -1;
}
}
//判断预处理语句是否为 insert 语句
return StringUtils.startsWithIgnoreCaseAndWs(sql, "INSERT", statementStartPos) && StringUtils.indexOfIgnoreCaseRespectMarker(statementStartPos, sql, "SELECT", "\"'`", "\"'`", false) == -1 && rewritableOdku;
}
从源码来看,JDBC 通过判断预处理语句是否为插入语句来决定是否重写结果。当然,除此之外,还有一些额外的判断,比如说,是否允许一次查询多条语句。
由于 ParseInfo 类为默认访问权限的内部类,并且,目前尚未发现能够对其进行自定义的方法,所以,无法对 buildRewriteInfo 进行修改。
由目前发现的问题来看,在使用 rewriteBatchedStatements 后,PreparedStatement 的批操作会发生较大的变化,而对于 Statement 而言,除了不能使用“;”外,并无较大的变化。
若在后续的探索中,发现了更多的问题,会更新到该文章中。
以上皆为本人学习时的笔录总结。若文章有错误之处,欢迎大家指正。