上篇文章我们分析到 PreparedStatementHandler,这篇文章我们就来详细的分析下 PreparedStatementHandler。
1.1 PreparedStatementHandler
/**
* @author Clinton Begin
*/
public class PreparedStatementHandler extends BaseStatementHandler {
public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
@Override
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
@Override
public void batch(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.addBatch();
}
// delegate.query(statement, resultHandler); 这句话就是调用的这句话
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute(); // sql 语句执行
return resultSetHandler.handleResultSets(ps); // 遍历查询结果集。
}
@Override
public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleCursorResultSets(ps);
}
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute(); // sql 语句执行
return resultSetHandler.handleResultSets(ps); // 遍历查询结果集。
}
上面这段代码就是去数据库中执行查询语句,并将结果集 的封装交给了 resultSetHandler 接口,接下来我们就来看看 ResultHandler 接口。
1.2 ResultHandler
/**
* @author Clinton Begin
*/
public interface ResultSetHandler {
// 传入 Statement 对象,返回 List
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
ResultSetHandler 接口就只有一个实现类 DefaultResultSetHandler
1.2.1 DefaultResultSetHandler
public class DefaultResultSetHandler implements ResultSetHandler {
....
//
// HANDLE RESULT SETS 查询 结果集的封装
//
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
// 到这里我们就知道了 我们查询结果的结果集是 一个 ArrayList ,本来这个没有啥问题,但是 要是和 pagehelper 一起 使用的话,可能会发生 分页的时候记录总数 错误的情况,后面我们会贴一段 pagehelper 的代码。
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 遍历处理每一行记录,并添加到 multipleResults 中
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);// 返回 multipleResults 对象。
}
@SuppressWarnings("unchecked")
private List<Object> collapseSingleResultList(List<Object> multipleResults) {
return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
}
.....
}
上面一段代码是对结果集的封装,方法里面有嵌套了其他方法来处理对象映射,这里我们就不细看里面的方法,我们会在后面来详细分析 参数映射 和结果集映射。这里我们的主线任务是追踪 查询过程,到这里我们就完成了主线任务。我们上面提到了 在使用 pagehelper 中会出现 查询总数错误的情况,结合上面的代码和 pagehelper 的代码我们来分析下原因。
1.3 pagehelper 分页示例
@Test
public void pageList() {
// PageHelper 分页要求分页的语句紧跟 PageHelper.startPage(1,10); 而且只对 第一条语句有效
PageHelper.startPage(1,10);
// 这里得到是Page 对象,Page对象 继承 ArrayList
List<User> userList = userMapper.selectAll();
PageInfo<User> userPageInfo = new PageInfo<>(userList);
}
上面是正常能获取到分页 数据的最简单写法,但是在很多时候我们需要 处理 List<User> userList = userMapper.selectAll(); 这个结果集,比如 会过滤掉 用户的密码,那么我们常规的做法可能 创建一个 UserVo 对象,把需要展示的 字段设置到 UserVo 对象里面去。
下面这种方式就会返回错误的分页数据:
@Test
public void pageListUserVo() {
PageHelper.startPage(1,2);
List<User> userList = userMapper.selectAll();
List<UserVo> userVoList = userList.stream().map(item -> UserVo.builder().username(item.getUsername()).id(item.getId()).build()).collect(Collectors.toList());
PageInfo<UserVo> userPageInfo = new PageInfo<>(userVoList);
int pages = userPageInfo.getPages();
long total = userPageInfo.getTotal();
System.out.println("pages = "+ pages);//总页数
System.out.println("total = "+ total);// 总的记录数
}
上面这种写法会经常在我们的开发中出现,首先我们来分析下出现错误的分页数据的原因。首先我们确定 发出的sql 语句是正确的 : Preparing: select id, username, password from sys_user LIMIT ? 。那么就是在 结果封装的时候出现了错误,也就是 new PageInfo<>()。
1.4 PageInfo
....
/**
* 包装Page对象
*
* @param list
*/
public PageInfo(List<T> list) {
this(list, 8);
}
/**
* 包装Page对象
*
* @param list page结果
* @param navigatePages 页码数量
*/
public PageInfo(List<T> list, int navigatePages) {
super(list);
if (list instanceof Page) { // 如果 list 是Page 对象的话执行这里,显然第一种分页是走的这里
//PageHelper.startPage(1,10);
//List<User> userList = userMapper.selectAll(); // 返回的是 Page 对象。
//PageInfo<User> userPageInfo = new PageInfo<>(userList);
Page page = (Page) list;
this.pageNum = page.getPageNum();
this.pageSize = page.getPageSize();
// 总页数和总的记录数
this.pages = page.getPages();
this.size = page.size();
//由于结果是>startRow的,所以实际的需要+1
if (this.size == 0) {
this.startRow = 0;
this.endRow = 0;
} else {
this.startRow = page.getStartRow() + 1;
//计算实际的endRow(最后一页的时候特殊)
this.endRow = this.startRow - 1 + this.size;
}
} else if (list instanceof Collection) { // 第二次的User->UserVo 显然是这里 Collectors.toList();返回的是 是 Collection接口,这里的分页总数都是通过 集合计算的
this.pageNum = 1;
this.pageSize = list.size();
this.pages = this.pageSize > 0 ? 1 : 0;// 总页数 1或者 0
this.size = list.size();// 总记录数是 集合的大小。
this.startRow = 0;
this.endRow = list.size() > 0 ? list.size() - 1 : 0;
}
if (list instanceof Collection) {
this.navigatePages = navigatePages;
//计算导航页
calcNavigatepageNums();
//计算前后页,第一页,最后一页
calcPage();
//判断页面边界
judgePageBoudary();
}
}
public class Page<E> extends ArrayList<E> implements Closeable {
private static final long serialVersionUID = 1L;
/**
* 页码,从1开始
*/
private int pageNum;
/**
* 页面大小
*/
private int pageSize;
/**
* 起始行
*/
private int startRow;
/**
* 末行
*/
private int endRow;
...
到这里我们就分析出了 为什么在 使用 分页插件的时候 会出现分页数据错误的情况,主要原因是 我们在User->UserVo 的时候 结果集 List 的类型发生了变化,插件返回的List 集合 其实是 Page 对象 ,而我们在转换了 后 返回的 是 以合 List 接口,然后进行 new PageInfo() 的时候,里面算出来的就不是我们想要的数据了。那么我们要怎么做呢?最简单的方式就是 我们在 User->UserVo 的时候也返回 Page 对象 比如这样: Page pageList = new Page(userVoList)。但是我们看完了 Page 对象 的所有方法包括构造器 ,都没有一个 方法可以让我们自己 设置 结果集 ,这点的话 springData jpa 就比较好,接口设计比较合理。当然我们也还有其他方式:(均来自 网络博客)
- mapper 对象 查询的时候 直接放回 xxxVo 对象。但是这种方式 可能导致的方法的复用性较低,不过也无可厚非。
- 结果集 查询先 放入 PageInfo 对象中,然后 Copy 数据到新的 PageInfo 对象。这种方式 可以复用 mapper 的查询接口,但是 一定程度上加大了代码的复杂度,不是很优雅。
@Test
public void pageListUserVo2() {
PageHelper.startPage(1, 2);
List<User> userList = userMapper.selectAll();
PageInfo<User> userPageInfo = new PageInfo<>(userList);// 先保存数据
List<UserVo> userVoList = userList.stream().map(item -> UserVo.builder().id(item.getId()).username(item.getUsername()).build())
.collect(Collectors.toList());
PageInfo<UserVo> userVoPageInfoFinal = new PageInfo<>(userVoList);
// 这里就是分页数据的 Copy 如总的页数,总的记录数
BeanUtils.copyProperties(userPageInfo, userVoPageInfoFinal);
int pages = userVoPageInfoFinal.getPages();
long total = userVoPageInfoFinal.getTotal();
System.out.println("pages = "+ pages);//总页数
System.out.println("total = "+ total);// 总的记录数
}
上面的思路很简单,先把分页数据保存下来,类型转换完后,复制 原来正确的分页数据到 最后的 PageInfo 对象中。如果还有其他处理方式,欢迎留言交流。
本片文章分析了 selectOne 最后的结果集封装,我们知道了 原生的 mybatis 结果是 封装到一个 ArrayList 中的,后面我们会详细分析 mybatis 的参数映射和 结果集的封装。此外我们还分析了 PageHelper 分页数据错误的原因和 解决方案,请持续关注后续!
网友评论