记一次关于jdbcTemplate.queryForList快速Debug及感悟
问题
开发环境报了一个错,如下:
Caused by: java.sql.SQLException: Invalid column index
at oracle.jdbc.driver.OraclePreparedStatement.setIntInternal(OraclePreparedStatement.java:6201)
at oracle.jdbc.driver.OraclePreparedStatement.setObjectCritical(OraclePreparedStatement.java:10252)
at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:9974)
at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:10799)
at oracle.jdbc.driver.OraclePreparedStatement.setObject(OraclePreparedStatement.java:10776)
at oracle.jdbc.driver.OraclePreparedStatementWrapper.setObject(OraclePreparedStatementWrapper.java:241)
at org.apache.commons.dbcp2.DelegatingPreparedStatement.setObject(DelegatingPreparedStatement.java:189)
at org.apache.commons.dbcp2.DelegatingPreparedStatement.setObject(DelegatingPreparedStatement.java:189)
at org.springframework.jdbc.core.StatementCreatorUtils.setValue(StatementCreatorUtils.java:402)
at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValueInternal(StatementCreatorUtils.java:232)
at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue(StatementCreatorUtils.java:163)
at org.springframework.jdbc.core.ArgumentPreparedStatementSetter.doSetValue(ArgumentPreparedStatementSetter.java:69)
at org.springframework.jdbc.core.ArgumentPreparedStatementSetter.setValues(ArgumentPreparedStatementSetter.java:50)
at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:662)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:603)
... 17 more
继续分析,是由于执行某个SQL时报错:
org.springframework.jdbc.InvalidResultSetAccessException: PreparedStatementCallback; invalid ResultSet access for SQL [...]; nested exception is java.sql.SQLException: Invalid column index
定位到代码中,是第3行的jdbcTemplate.queryForList
产生的问题。
1 try {
2 String sql = "SELECT SOME_THING FROM SOME_TABLE WHERE CONDITION = " + CONDITION_A;
3 List<String> results = jdbcTemplate.queryForList(sql, new Object[]{CONDITION_A}, String.class);
4 String value = results.get(0);
5 doSomeThing(value);
6 } catch (Exception e) {
7 printError(e);
8 }
分析1
首先,我怀疑这个SQL本身是不是有问题,导致跑不出结果。
于是我把这个SQL手动在DB里跑了一遍,发现没有问题,会返回给我一个String ABC。
SELECT SOME_THING FROM SOME_TABLE WHERE CONDITION = CONDITION_A;
-- result: ABC
这个原因排除,继续分析。
分析2
当我看到第五行,results.get(0)
,心中不禁有个疑问,这里会不会抛NPE?但是这和上文的问题不想关,所以暂且把这个点记录下来,待会再做改进。
接着,我上网搜了一下关于java.sql.SQLException: Invalid column index
的常见原因。
-> https://javarevisited.blogspot.com/2012/01/javasqlsqlexception-invalid-column.html
它说,主要是在set prepared statement的时候,值没配好,例子如下:
public static void main(String args[]) throws SQLException {
Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1526:TESTID", "root", "root");
PreparedStatement preStatement = conn.prepareStatement("select distinct item from Order where order_id=?");
preStatement.setString(0, "123456"); //this will throw "java.sql.SQLException: Invalid column index" because "0" is not valid colum index
ResultSet result = preStatement.executeQuery();
while(result.next()){
System.out.println("Item: " + result.getString(2)); //this will also throw "java.sql.SQLException: Invalid column index" because resultset has only one column
}
}
但是这里我们是用spring jdbcTemplate做query,值是由spring框架自动填进去的,应该不会有问题呀?
分析3
然后,我怀疑这里关于jdbcTemplate.queryForList
的用法是不是有问题?于是,我在项目中其它working的代码中找有没有类似的用法。
发现,其它的地方也是类似的用法。
找到问题
深呼吸,一定有什么地方漏掉了。
这句话List<String> results = jdbcTemplate.queryForList(sql, new Object[]{CONDITION_A}, String.class);
有问题,
但是,List<String> results
没问题,
jdbcTemplate.queryForList
也没问题,
那么,唯一有问题的,只能是传入的参数,sql, new Object[]{CONDITION_A}, String.class
。
结合queryForList
的源码如下,分析传参。
@Override
public <T> List<T> queryForList(String sql, Object[] args, Class<T> elementType) throws DataAccessException {
return query(sql, args, getSingleColumnRowMapper(elementType));
}
方法定义:
- sql:需要执行的sql语句
- args:sql内置的参数
- elementType:返回值类型
实际传入参数:
- sql:
SELECT SOME_THING FROM SOME_TABLE WHERE CONDITION = CONDITION_A
- args:
CONDITION_A
- elementType:
String.class
我发现,实际传入的sql和args有重叠的地方。既然sql已经是完整的了,已经有CONDITION_A
了,为什么还需要一个CONDITION_A
作为参数传入呢?
问题原来在这!
所以后来Spring在parse参数的时候报错了,因为没有找到?
。(回头想一想,这个问题确实挺隐蔽的,乍一看方法签名一点问题没有)
解决
解决也很简单,有两种。
- 使用不包含args的
queryForList
API:
this.jdbcTemplate.queryForList(sql, String.class);
- 更改sql,使其留出placeholders的位置:
String sql = "SELECT SOME_THING FROM SOME_TABLE WHERE CONDITION = ?";
10分钟改完了重新测试,果然解决了问题。
总结
Debug方法千千万,有三点很重要。
- 一是发散思维,将问题和固有经验结合,看能不能找到类似的案例。
- 二是逻辑思维,一步一步,按图索骥,要把问题一步步narrow down到最小的粒度。
- 三是检索能力,google出来前10个结果,可能只有1-2个能提供到一些实际帮助,需要仔细甄别。