今天在写底层数据库操作的时候遇到了一个异常,异常的解决办法我实际上早就有了,只是不明白为什么为报这个异常,激起我求知的欲望。
问题描述:项目底层的增加、删除、修改都采用了QueryRunner提供的update()来实现,关键问题就来了,我用拼好的删除语句(deleteSQL)可以完美执行,但是在测试拼接完整的insert插入语句的时候却通不过,控制台输出的信息Wrong number of parameters: expected 3, wasgiven 0。这里我没有采用常规的占位符,而是直接在拼好SQL语句扔进update执行。
通过错误提示:可以大胆猜测,可能是底层对insert语句进行了特殊处理,把value里面的内容解析为了占位符,或者底层要求insert语句必须采用占位符这种情况。从网上下载相应的源码包,我们通过源码进入调试一条一条分析源码。
public int update(Connection conn, String sql) throws SQLException {
return this.update(conn, false, sql, (Object[]) null);
}
可以看到调用update实际上还是调用带有参数的方法,只是参数为空而已。
private int update(Connection conn, boolean closeConn, String sql, Object... params) throws SQLException {
if (conn == null) {
throw new SQLException("Null connection");
}
if (sql == null) {
if (closeConn) {
close(conn);
}
throw new SQLException("Null SQL statement");
}
PreparedStatement stmt = null;
int rows = 0;
try {
stmt = this.prepareStatement(conn, sql);
this.fillStatement(stmt, params);
rows = stmt.executeUpdate();
} catch (SQLException e) {
this.rethrow(e, sql, params);
} finally {
close(stmt);
if (closeConn) {
close(conn);
}
}
return rows;
}
从这个方法中可以看到关键是try块里面的内容,这里面调用prepareStatement,这一步应该是一个预处理。下面的这个方法应该是关键所在,也就是填充参数的时候。
public void fillStatement(PreparedStatement stmt, Object... params) throws SQLException {
// check the parameter count, if we can
ParameterMetaData pmd = null;
if (!pmdKnownBroken) {
pmd = stmt.getParameterMetaData();
int stmtCount = pmd.getParameterCount();
int paramsCount = params == null ? 0 : params.length;
if (stmtCount != paramsCount) {
throw new SQLException("Wrong number of parameters: expected "
+ stmtCount + ", was given " + paramsCount);
}
}
// nothing to do here
if (params == null) {
return;
}
for (int i = 0; i < params.length; i++) {
if (params[i] != null) {
stmt.setObject(i + 1, params[i]);
} else {
// VARCHAR works with many drivers regardless
// of the actual column type. Oddly, NULL and
// OTHER don't work with Oracle's drivers.
int sqlType = Types.VARCHAR;
if (!pmdKnownBroken) {
try {
sqlType = pmd.getParameterType(i + 1);
} catch (SQLException e) {
pmdKnownBroken = true;
}
}
stmt.setNull(i + 1, sqlType);
}
}
}
报错信息正好来至这个方法中,也就是这个stmtCount的值为参数的个数,在SQLServerParameterMetaData类中我们也看到了如下方法
private MetaInfo parseStatement(String paramString)
/* */ throws SQLServerException
/* */ {
/* 255 */ StringTokenizer localStringTokenizer = new StringTokenizer(paramString, " ");
/* 256 */ if (localStringTokenizer.hasMoreTokens())
/* */ {
/* 258 */ String str = localStringTokenizer.nextToken().trim();
/* */
/* 260 */ if (str.equalsIgnoreCase("INSERT")) {
/* 261 */ return parseStatement(paramString, "INTO");
/* */ }
/* 263 */ if (str.equalsIgnoreCase("UPDATE")) {
/* 264 */ return parseStatement(paramString, "UPDATE");
/* */ }
/* 266 */ if (str.equalsIgnoreCase("SELECT")) {
/* 267 */ return parseStatement(paramString, "FROM");
/* */ }
/* 269 */ if (str.equalsIgnoreCase("DELETE")) {
/* 270 */ return parseStatement(paramString, "FROM");
/* */ }
/* */ }
/* 273 */ return null;
/* */ }
private MetaInfo parseStatement(String paramString1, String paramString2)
/* */ {
/* 213 */ StringTokenizer localStringTokenizer = new StringTokenizer(paramString1, " ,", true);
/* */
/* 217 */ String str1 = null;
/* 218 */ String str2 = "";
/* 219 */ while (localStringTokenizer.hasMoreTokens())
/* */ {
/* 221 */ String str3 = localStringTokenizer.nextToken().trim();
/* */
/* 223 */ if (str3.equalsIgnoreCase(paramString2))
/* */ {
/* 225 */ if (localStringTokenizer.hasMoreTokens())
/* */ {
/* 227 */ str1 = escapeParse(localStringTokenizer, localStringTokenizer.nextToken());
/* 228 */ break;
/* */ }
/* */ }
/* */ }
/* */
/* 233 */ if (null != str1)
/* */ {
/* 235 */ if (paramString2.equalsIgnoreCase("UPDATE"))
/* 236 */ str2 = parseColumns(paramString1, "SET");
/* 237 */ else if (paramString2.equalsIgnoreCase("INTO"))
/* 238 */ str2 = parseInsertColumns(paramString1, "(");
/* */ else {
/* 240 */ str2 = parseColumns(paramString1, "WHERE");
/* */ }
/* 242 */ return new MetaInfo(str1, str2);
/* */ }
/* */
/* 245 */ return null;
/* */ }
这些方法都会通过预处理SQL语句,而处理insert插入语句的时候,是通过INTO后面的()的内容来判断,而里面放的是插入的列名,各种列的名字肯定不同,所以断定是通过“,”来分割实现统计参数的个数,VALUES(后面的内容在处理的时候其实并没有用到,接下来就是参数的补齐,最后加上“)”一个完整的insertSQL语句就处理完成,与?占位符没有多大的关系。那么delete又是怎么能够正确完成的呢?测试后发现delete判断参数的时候是采用判断?个数,但也不全是问号的个数,必须是正确的写法,而且还能够检查列名是否正确,进一步保证了参数个数的正确性。所有用prepareStatement写的都会验证列名是否写正确。