jdbcTemplate.queryForList的坑

2,250 阅读2分钟

一、问题背景

相信大家在项目一定使用过jdbcTemplate操作数据库,因业务需求,需要查询集合对象List<Entity>,于是看到jdbcTemplate有个queryForList方法,就直接用了,殊不知,这里有个大坑等着我去跳。

错误写法:

StringBuilder sql = new StringBuilder(); 
sql.append("SELECT refBizId,memberId,applyMobile FROM table where xxxx"); 
List<Entity> list = jdbcTemplate.queryForList(sql, Entity.class);

提测之后,发现查询就报错了,喜提bug。

堆栈日志:

org.springframework.jdbc.IncorrectResultSetColumnCountException: Incorrect column count: expected 1, actual 3
    at org.springframework.jdbc.core.SingleColumnRowMapper.mapRow(SingleColumnRowMapper.java:92)
    at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:93)
    at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:60)
    at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:703)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:639)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:690)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:717)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:767)
    at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.query(NamedParameterJdbcTemplate.java:192)
    at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForList(NamedParameterJdbcTemplate.java:250)
    at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForList(NamedParameterJdbcTemplate.java:257)

二、错误原因

直接debug查看源码报错处mapRow方法。nrOfColumn含义为查询列的数量,这里我们看到取值为3,对应我们的sql语句 SELECT refBizId,memberId,applyMobile 发现确实为3个字段。 通过查看源码注释,其实发现已经提示我们只能查询单列了,实际源码中会判断if (nrOfColumns != 1),只允许查询1个字段,否则会抛IncorrectResultSetColumnCountException异常。

image.png

三、正确姿势

方法一:

如果仅需查询单个字段,可以使用queryForList方法。

jdbcTemplate.queryForList(sql, params, Integer.class); // 数据库字段对应是int类型
jdbcTemplate.queryForList(sql, params, String.class); // 数据库字段对应是varchar类型

方法二:

使用query(String sql, RowMapper<T> rowMapper)方法,自己实现RowMapper接口。

/**
 * Execute a query given static SQL, mapping each row to a result object
 * via a RowMapper.
 * <p>Uses a JDBC Statement, not a PreparedStatement. If you want to
 * execute a static query with a PreparedStatement, use the overloaded
 * {@code query} method with {@code null} as argument array.
 * @param sql the SQL query to execute
 * @param rowMapper a callback that will map one object per row
 * @return the result List, containing mapped objects
 * @throws DataAccessException if there is any problem executing the query
 * @see #query(String, Object[], RowMapper)
 */
<T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException;

这里给出1.7和1.8两种实现:

jdk1.7:
list = jdbcTemplate.query(sql.toString(), params, new RowMapper<Apply>() {
    @Override
    public CashFinalApply mapRow(ResultSet rs, int rowNum) throws SQLException {
        Apply apply = new Apply();
        apply.setRefBizId(rs.getString("refBizId"));
        apply.setMemberId(rs.getString("memberId"));
        apply.setApplyMobile(rs.getString("applyMobile"));
        return applypply;
    }
});
 
jdk1.8:
list = jdbcTemplate.query(sql.toString(), params, (rs, rowNum) -> {
        Apply apply = new Apply();
        apply.setRefBizId(rs.getString("refBizId"));
        apply.setMemberId(rs.getString("memberId"));
        apply.setApplyMobile(rs.getString("applyMobile"));
        return applypply;
    }
});

方法三(推荐):

使用public <T> List<T> query(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)方法,BeanPropertyRowMapper实现了RowMapper接口,使用该构造函数初始化BeanPropertyRowMapper对象。

public class BeanPropertyRowMapper<T> implements RowMapper<T>{}
 
/**
 * Create a new {@code BeanPropertyRowMapper}, accepting unpopulated
 * properties in the target bean.
 * <p>Consider using the {@link #newInstance} factory method instead,
 * which allows for specifying the mapped type once only.
 * @param mappedClass the class that each row should be mapped to
 */
public BeanPropertyRowMapper(Class<T> mappedClass) {
   initialize(mappedClass);
}

实际使用:

// guava ImmutableMap 你也可以使用new HashMap
Map<String, Object> params = ImmutableMap.of("memberId", memberId);
List<Entity> list = jdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(Entity.class));

好了,到这里我们把这个坑填上了,江湖再见~