今天在学springboot的时候,学到了数据访问这块,然后在写了一个demo,随手就用springboot-data-starter中自带的JdbcTemplate模板。用了之后发现总是查询不到所有的数据。
实体类:
public class User {
private Long id;
private String username;
private String password;
private Integer score;
//省略get,set方法
//省略toString方法
}
Dao层:
@Component
public class UserDaoImpl implements IUserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public List findList(){
String sql = "select * from user";
List users = jdbcTemplate.queryForList(sql,User.class);
return users;
}
}
然后发现执行sql语句,总是出错:
org.springframework.jdbc.IncorrectResultSetColumnCountException: Incorrect column count: expected 1, actual 4
at org.springframework.jdbc.core.SingleColumnRowMapper.mapRow(SingleColumnRowMapper.java:92) ~[spring-jdbc-4.3.23.RELEASE.jar:4.3.23.RELEASE]
at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:93) ~[spring-jdbc-4.3.23.RELEASE.jar:4.3.23.RELEASE]
异常显示 错误的列数:预期的1,实际的4
本来刚开始看到这个异常的时候,一时没反应过来什么错误,因为一直是用惯了MyBaits来操作数据库,看到JdbcTemplate模板有个queryForList方法,就简单的以为是把查询结果转成List集合,用了(sql,entityClass)这个方法。然后就出异常了。
翻了翻源码,才发现这里是用于从单个列读取结果对象。
/**
* Create a new RowMapper for reading result objects from a single column.
* @param requiredType the type that each result object is expected to match
* @return the RowMapper to use
* @see SingleColumnRowMapper
* 注释翻译:创建一个新的行映射器,用于从单个列读取结果对象。
*/
protected RowMapper getSingleColumnRowMapper(Class requiredType) {
return new SingleColumnRowMapper(requiredType);
}
那么就清楚了,单个列只能是基础类型或者String复杂类型,比如Integer.class,String.class。
那么在看看源码,找到了这个:
@Override
public T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException {
List results = query(sql, rowMapper);
return DataAccessUtils.requiredSingleResult(results);
}
这个是查询数据,然后返回值是个Object对象。而DataAccessUtils.requiredSingleResult(results)方法是干什么的呢?看注释:
/**
* Return a single result object from the given Collection.
* Throws an exception if 0 or more than 1 element found.
* @param results the result Collection (can be {@code null})
* @return the single result object
* @throws IncorrectResultSizeDataAccessException if more than one
* element has been found in the given Collection
* @throws EmptyResultDataAccessException if no element at all
* has been found in the given Collection
* 从给定集合中返回单个result对象。
* 抛出一个异常,如果发现0或超过1个元素
*/
public static T requiredSingleResult(Collection results) throws IncorrectResultSizeDataAccessException {
if (CollectionUtils.isEmpty(results)) {
throw new EmptyResultDataAccessException(1);
}
if (results.size() > 1) {
throw new IncorrectResultSizeDataAccessException(1, results.size());
}
return results.iterator().next();
}
那么我们就明白了,这个方法只能返回一条数据。所以这个方法是先查询全部的数据,然后在判断结果集里数据的长度是否为0或者大于1,就抛异常。
所以我们要查询只查询一条数据就可以用这个
queryForObject(String sql, RowMapper rowMapper)
方法,而查询全部呢,肯定是这个方法
List results = query(sql, rowMapper);
那么这个rowMapper是什么对象呢,继续分析源码:
public interface RowMapper {
/**
* Implementations must implement this method to map each row of data
* in the ResultSet. This method should not call {@code next()} on
* the ResultSet; it is only supposed to map values of the current row.
* @param rs the ResultSet to map (pre-initialized for the current row)
* @param rowNum the number of the current row
* @return the result object for the current row (may be {@code null})
* @throws SQLException if a SQLException is encountered getting
* column values (that is, there's no need to catch SQLException)
* 实现必须实现此方法来映射每一行数据结果集。此方法不应在上调用结果集;它只应该映射当前行的值。
*/
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
RowMapper是一个接口,里面只有一个方法,注释上给咱们写的清清楚楚:映射每一行数据结果集。
那么我们就需要自己来实现这个接口,这个接口只有这一个方法,而这个方法的参数大家肯定感到非常熟悉。
Java的原生JDBC在执行完sql语句,所返回的结果集就是一个java.sql.ResultSet对象,我们对这个结果集可以根据下标或者字段名的形式来取值。
public class UserRowMapper implements RowMapper {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong(1));
user.setUsername(rs.getString(2));
user.setPassword(rs.getString(3));
user.setScore(rs.getInt(4));
return user;
}
}
写完这个类之后,然后我们再改一下dao层的方法。
public List findList(){
String sql = "select * from user";
List users = jdbcTemplate.query(sql,new UserRowMapper());
return users;
}
然后再次重启程序,就可以得到数据了。
[{"id":1,"username":"tom","password":"123","score":81},{"id":2,"username":"root","password":"123456","score":80}]
因为我正在学springboot这个约定大于配置的框架。就想看看spring有没有写好的rowMapper实现类以供我们使用呢?
打开RowMapper这个接口的继承树,发现还真有。。。(我自己都没想到会有)
实现类还不少。
RowMapper有一个实现类是SingleColumnRowMapper。
public class SingleColumnRowMapper implements RowMapper {
@Override
@SuppressWarnings("unchecked")
public T mapRow(ResultSet rs, int rowNum) throws SQLException {
// Validate column count.
ResultSetMetaData rsmd = rs.getMetaData();
int nrOfColumns = rsmd.getColumnCount();
if (nrOfColumns != 1) {
throw new IncorrectResultSetColumnCountException(1, nrOfColumns);
}
//列数只要不等于1就抛异常
// Extract column value from JDBC ResultSet.
Object result = getColumnValue(rs, 1, this.requiredType);
if (result != null && this.requiredType != null && !this.requiredType.isInstance(result)) {
// Extracted value does not match already: try to convert it.
try {
return (T) convertValueToRequiredType(result, this.requiredType);
}
catch (IllegalArgumentException ex) {
throw new TypeMismatchDataAccessException(
"Type mismatch affecting row number " + rowNum + " and column type '" +
rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
}
}
return (T) result;
}
看类名就大概可以猜到只能返回单一的列。多的少的都不行,都会抛出异常。而ColumnMapRowMapper这个类指的是把结果集映射成一个Map对象来返回:
public class ColumnMapRowMapper implements RowMapper
mapRow方法里是new了一个Map
而我们需要的返回值是个实体类,是这个类BeanPropertyRowMapper(看类名就看出来啦,返回值肯定是JavaBean)。
修改代码:
public List findList(){
String sql = "select * from user";
List users = jdbcTemplate.query(sql,new BeanPropertyRowMapper());
return users;
}
然后就报错了。。。
java.lang.IllegalStateException: Mapped class was not specified
意思是说未指定映射类,可是我们不是加了泛型为User类的么,在看看这个类所实现的mapRow方法具体实现:
/**
* Extract the values for all columns in the current row.
* Utilizes public setters and result set meta-data.
* @see java.sql.ResultSetMetaData
*/
@Override
public T mapRow(ResultSet rs, int rowNumber) throws SQLException {
Assert.state(this.mappedClass != null, "Mapped class was not specified");
T mappedObject = BeanUtils.instantiateClass(this.mappedClass);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
initBeanWrapper(bw);
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
Set populatedProperties = (isCheckFullyPopulated() ? new HashSet() : null);
//省略下面的代码,太多了
}
这个方法第一行就是断言。
我们发现异常就是这里抛出的。是因为
this.mappedClass != null
那么我们就需要给这个参数传递一个映射类。
哟,看了看构造方法,刚好有这个参数的构造器。
改代码:
public List findList(){
String sql = "select * from user";
List users = jdbcTemplate.query(sql,new BeanPropertyRowMapper(User.class));
return users;
}
运行结果:
[{"id":1,"username":"tom","password":"123","score":81},{"id":2,"username":"root","password":"123456","score":80}]
nice,总结几点:
1.也算是给我自己的建议吧,以后看到错误,最好是多看源码,多分析,多思考,不能毛毛躁躁的了。
2.在映射类的结果上,归根究底还是RowMapper这个接口,底层数据库表和Java实体类映射,还是ResultSet游标来操作的。
3.spring为我们实现了多种情况的实现类,我们可以自己实现这个接口,也可能用spring为我们实现的。