关于 MySQL JDBC rewriteBatchedStatements使用的一些注意事项

以下操作基于 JDK1.8 ,以及 JDBC 5.1.48 完成。

前言

在 MySQL JDBC 中,批操作的提交默认是逐条进行的。而在链接中加入以下参数,会把多条语句合并成一条提交。当 SQL 语句累积到一定数量(由数据库可接受的最大数据包大小决定),再一次性提交到数据库,减少了与数据库的交互次数,大大提高了效率。

rewriteBatchedStatements = true

使用注意事项

下面说一下,在使用 rewriteBatchedStatements 的时候,需要注意的事项。

1. 批操作语句数大于特定值,才合并

下面有请源码:

Statement

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),才会合并成一条语句提交。

PreparedStatement

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),才会合并成一条语句提交。

2. 批操作语句不能加“;”

在实际中,我们会遇到自己或同事写的 SQL 语句末尾有一个分号的情况,用起来也没有什么问题。但在链接加上 rewriteBatchedStatements=true ,则会在运行时报错。
关于 MySQL JDBC rewriteBatchedStatements使用的一些注意事项_第1张图片
为什么会这样呢?下面有请源码:

Statement

private int[] executeBatchUsingMultiQueries(boolean multiQueriesEnabled, int nbrCommands, int individualStatementTimeout) throws SQLException {
	...
	for(commandIndex = 0; commandIndex < nbrCommands; ++commandIndex) {
		...
		queryBuf.append(nextQuery);
		queryBuf.append(";");
		++argumentSetsInBatchSoFar;
	}
	...
}

PreparedStatement

protected int[] executePreparedBatchAsMultiStatement(int batchTimeout) throws SQLException {
    synchronized(this.checkClosed().getConnectionMutex()) {
      	if (this.batchedValuesClause == null) {
        	this.batchedValuesClause = this.originalSql + ";";	//在 SQL 语句末尾加上分号
      	}
		...
	}
}

我们可以看到,对于批操作,JDBC 会在 SQL 语句的末尾加上分号。

batchedValuesClause 默认为 null,由于能力有限,目前仍找不到修改的办法,找到后会更新上来。

3. PreparedStatement 的批量插入操作返回值的每一项为总成功数

有时,我们会用到 executeBatch 的返回值,该方法会返回一个 int 数组,其中,第 n 项代表着第 n 条语句是否执行成功(0,1)。

但在使用 rewriteBatchedStatements 后,对于 PreparedStatement 的批量插入操作来说,它返回的每一项代表着整个批操作的总成功数。
关于 MySQL JDBC rewriteBatchedStatements使用的一些注意事项_第2张图片
下面再次有请源码:

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 而言,除了不能使用“;”外,并无较大的变化。

若在后续的探索中,发现了更多的问题,会更新到该文章中。

以上皆为本人学习时的笔录总结。若文章有错误之处,欢迎大家指正。

你可能感兴趣的:(Java)