最近同事在讨论一个关于分页的话题,我在此简单整理一下对于分页的认识。
首先,分页是什么层面上的事儿?是数据访问层面、业务层面还是展示层面?
对于数据访问层来说,具体说,对于查询接口,需要一个“from” 参数和一个“to” 参数,就可以做到获取查询结果集中特定的记录了,它不应该知道任何关于第几页和每页有几条数据这样的信息,这种信息应该是在上层的展示层面所关心的。
举例来说,有这样的接口调用(这只是其中一种接口形式,关于 DAO 接口的形式可以参见这篇文章的讨论):
map.put("age", 18); map.put("from", 3); map.put("to", 5); List<User> userList = userDao.list(map);
其次,可以达成共识的是,分页功能应该由一个工具来实现,这个工具不应该知道任何业务逻辑,应该与其它部分解耦开。
分页工具可以是一个简单的计算工具,连实际的数据都不需要给它,只需要指定总数和每页大小:
class PaginationSupport { //构造器 public PaginationSupport(int totalCount, int pageSize){} //翻页 public void turnToNextPage(){} public void turnToPreviousPage(){} public void setPageNo(int pageNo){} //数据访问层需要的 start 和 end public int getStart(){} public int getEnd(){} //是否还有上一页、下一页 public boolean hasNextPage(){} public boolean hasPreviousPage(){} //当前在哪一页 public int getPageNo(){} ... ... }
翻页任意次之后,调用 getStart/getEnd 方法就能输出 DAO 需要的“start” 和“end” 参数的值。这是一种最纯粹的分页工具了,实现也非常简单,下面我们来把它弄复杂一点。
如果在内存里分页,借助模板参数,稍稍修改一下成下面这种形式,把数据给它,让它来帮你返回显示页需要的结果集合:
class PaginationSupport<T> { //构造器 public PaginationSupport(List<T> items, int pageSize){} //其它主要方法不变 ... ... }
可是,这种形式需要置入一个 List<T>,如果我想在数据库分页,而非内存分页,这种形式就做不到了。那能不能提供一种兼容二者的通用方式呢?
可以。引入这样一个接口:
interface DataCollection<T>{ public List<T> getData(int start, int end); public int getCount(); }
而 PaginationSupport 这个类统一成如下形式:
class PaginationSupport<T> { //构造器 public PaginationSupport(DataCollection<T> dc, int pageSize, int startPageNo){} //获取当前页数据 public List<T> getData(){} ... ... }
这样一来,如果是内存分页,就在 DataCollection 接口的实现中,操纵这个数据 list:
PaginationSupport ps = new PaginationSupport<User>(new DataCollection<User>() { List<User> list = xxx; public int getCount() { return list.size(); } public List<Object> getData(int start, int end) { return list.subList(start, end); } }, 10);
而如果是需要 DAO 层 start 和 end 参数传入的分页,则在 DataCollection 接口的实现中,调用 DAO:
PaginationSupport ps = new PaginationSupport<User>(new DataCollection<User>() { public int getCount() { return userDao.count(); } public List<User> getData(int start, int end) { ... ... map.put("start", start); map.put("end", end); return userDao.list(map); } }, 10);
需要补充的是,如果只需要下面这种调用 DAO 接口来实现的查询分页,分页的工作还可以进一步改进,因为将“start” 和“end” 这两个参数注入 map 的过程,完全可以让框架来完成—— 从分页工具开始,直到数据库访问的 SQL 代码为止,开发人员都可以不关心这两个参数,让框架来拼接这个查询子句。这样的话,DataCollection 接口就会变成这个样子:
interface DataCollection<T>{ public List<T> getData(Map queryMap); public int getCount(); }
其中 getData 方法的 queryMap 参数,分页工具已经给预置好了一些协助查询的参数,开发人员不需要手动构造和添加这样的参数了。
好处仅仅是这么多吗?不是。分页工具只是做分页这一件事没错,但是框架可以利用它,在外面做很多额外的事情。比如,在接口改成如上的形式时,我们还可以做到对分页查询结果的缓存完全透明化,开发人员连缓存条目的 key 都不需要提供。如果我们规约好查询方法都叫“list”,框架获知当前查询的模型 T,完整查询的 queryMap 对象,以这两个作为生成 key 的因子(如果该模型的查询方法不止一个,那就还需要查询方法名也作为因子提供),比如生成了这样一个 key:
User_age=18_start=3_end=7
之后就可以根据一定的配置帮助缓存起查询的结果来,比如:
com.company.xxx.User.list=30000
表示 User 的查询缓存记录的过期时间是 30000 毫秒。
最后来说说查询子句不一致的问题。分页查询的 SQL 子句大不相同,比如 Oracle 会用到 rownum,而 MySQL 又需要 limit,所以一种方法是在 DAO 层屏蔽这样的差异。
不过,还有一个思路是利用 JDBC 来屏蔽差异,它提供了结果集对象,很适合我们做这件事(java.sql.ResultSet 接口的 next 和 previous 方法),比如我们可以自己定义一个名为 PageableResultSet 这样的接口来继承自 ResultSet 接口。不过需要注意的是,这个接口可没有提供跳到任意记录的方法,因此在实现的时候只能借由游标走,一行一行记录地 next 或者 previous。我见过的几个项目都没有用这个方式来实现的,关于它的优劣,欢迎你来给我分析分析。
——————————————————————————————————————–
2012 年 11 月 13 日:
momo 回复给出了一个分页方式讨论的链接,其中提到一种方式是“ 二次 top”,其实这也并没有什么特别的,从一个查询的结果集合中再取数据:
set @sqlstr = 'SELECT TOP ' + Str(@pagesize) + ' * from r_student where id not in'; set @sqlstr = @sqlstr + '(SELECT TOP '+ Str((@currentpage-1)*@pagesize) + ' id from r_student order by Id)'
还有就是存储过程了。
文中给出的测试结论是,还是使用结果集的游标移动来实现分页获取数据的方法是最快的。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
http://www.cnblogs.com/eaglet/archive/2008/10/09/1306806.html
ResultSet 这个看起来和 DataReader 有点像啊。。
在.net 里面似乎这种方式可取。
我觉得可以分开来,先区 top n,然后超过这个 n 的再把 top 加倍,
比如对于某个条件 x
第一次查询的时候 sql 为 select top n xx from table where x
用 datareader 去 某 page 内的数据
然后翻页翻到超过 n 时
为 select top 2n xx from table where x
。。。。。