mybatis分页的一种解决方案
要想写好一个功能,哪怕这个功能很简单,也要考虑到扩展性。
最好的学习路线是从具体到抽象,而最好的设计思路是从抽象到具体。
一、抽象输入输出
对于一个功能,使用者只需要关心输入和输出即可。
分页查询
输入:
页码和每页的记录条数(必需)
排序规则(非必需)
查询条件(非必需)
输出:
记录集合
记录总条数
总页数
由此可设计出两个类,分别对应分页操作的输入和输出
PageReqeust:
/**
* Created by liuruijie on 2017/4/6.
* 分页查询的相关参数封装
*/
public class PageRequest {
private int size; //查询记录的数量
private Sort[] sorts; //排序
private int start; //开始位置
//page为页码
public PageRequest(int page, int size, Sort... sorts) {
this.size = size;
this.sorts = sorts;
this.start = (page-1)*size;
}
public static class Sort {
private String type;
private String field;
public Sort(String field) {
this.field = field;
type = "ASC";
}
public Sort(String field, String type) {
this.field = field;
this.type = type;
}
···
}
//省略set和get方法
···
}
该类封装了分页查询的页码和页面大小,还有排序规则。对于查询条件,由于其需要由具体的实体和具体的业务才能确定,所以不方便在这里封装。
Page:
/**
* Created by liuruijie on 2017/4/6.
* 分页查询的返回值
*/
public class Page<T> {
private long totalRows; //总记录数
private int totalPages; //总页数
private List<T> rows; //查询到的记录
//省略set和get方法
···
}
该类封装了分页查询需要的结果。
到现在,分页的输入和输出都定义好了,需要进行过程的实现。
二、定义接口规范
同样的,先抽象分页操作
/**
* Created by liuruijie on 2017/4/17.
* 提供基础的分页接口
*/
public interface PageService<T> {
Page<T> selectPage(PageRequest request);
}
一般的分页(没有查询条件)都可以走这个方法。
分页最顶层的接口已经定义好了,接下来就要设计dao层了。
使用mybatis的注解方式不用在xml里写sql的映射,直接在方法上写注解,给出sql即可。
还是先给出一个接口规范。
/**
* Created by liuruijie on 2017/4/17.
* 对于需要映射分页sql的mapper
* ,给出一个规范
*/
public interface PageMapper<T> {
List<T> findAll(@Param("page") PageRequest request);
Long countAll();
}
两个方法,一个查数据,一个查数量。
三、提供默认实现
本文不详细说明mybatis的使用,具体使用请看官方文档。
mybatis的注解方式映射sql,使用方式,在最下面:
www.mybatis.org/mybatis-3/zh/java-api.html
然后先不着急写具体的mapper接口,采用mybatis @SelectProvider注解来绑定查询,写一个分页的provider。
/**
* Created by liuruijie on 2017/4/6.
* 提供默认的分页列表查询
*/
public abstract class PageSqlProvider {
protected abstract SQL preSql();
//默认的分页列表查询
public String findAll(@Param("page") PageRequest request){
return findByCase(request, preSql().SELECT("*"));
}
//默认的计数查询
public String countAll(){
return countByCase(preSql());
}
//用于拼接条件的分页列表查询,在子类中设置条件,sql为已拼接了条件的SQL对象。
protected String findByCase(@Param("page") PageRequest request, SQL sql){
if(request.getSorts()!=null&&request.getSorts().length!=0){
for(int i=0;i<request.getSorts().length;i++){
PageRequest.Sort sort = request.getSorts()[i];
sql.ORDER_BY(sort.getField()+" "+sort.getType());
}
}
String preSql = sql.toString();
StringBuilder sb = new StringBuilder(preSql);
sb.append(" limit #{page.start},#{page.size}");
return sb.toString();
}
//用于拼接条件的计数查询,在子类中设置条件,sql为已拼接了条件的SQL对象。
protected String countByCase(SQL sql){
return sql.SELECT("count(*)").toString();
}
}
这是一个抽象类,预留了一个preSql方法,主要是让子类去设置表名。需要注意的一点,分页在不同数据库中的实现可能不同,因此,mybatis提供的SQL类中并没有分页相关的sql拼接,需要自己拼接。mysql中的分页是使用limit关键字。
而把具体的分页部分的代码提出来放到另外的方法中的目的是为之后的条件查询提供方便。
到此,分页最底层的逻辑都已经写好,可以放到具体的实体中应用了。
四、引入具体业务
设计一个user表,并插入数据:
user表结构及数据编写一个UserInfo实体类映射表中的字段,代码省略。
编写一个UserMapper接口,继承PageMapper接口,并指定泛型为UserInfo:
public interface UserMapper extends PageMapper<UserInfo>{
String tableName = "sys_user";
@SelectProvider(type = UserSqlProvider.class, method = "findAll")
List<UserInfo> findAll(@Param("page") PageRequest pageRequest);
@SelectProvider(type = UserSqlProvider.class, method = "countAll")
Long countAll();
class UserSqlProvider extends PageSqlProvider{
@Override
protected SQL preSql() {
return new SQL().FROM(tableName);
}
}
}
其中UserSqlProvider继承PageSqlProvider,获得了findAll,以及countAll方法。只需在preSql方法中给定表名即可。而对于UserMapper接口的findAll和countAll方法,可以直接用@SelectProvider指定对应的sql为UserSqlProvider类的findAll和countAll方法的返回值。
mapper暂时实现到此,接下来回到PageService接口,这个接口唯一的与具体实体相关的参数是泛型参数,而selectPage接口方法并不需要与具体的实体相关。基于这个特点,可以仿照PageSqlProvider编写一个默认的实现。
/**
* Created by liuruijie on 2017/4/17.
* 基本分页的默认实现
*/
public abstract class PageServiceAdapter<T> implements PageService<T> {
//此mapper由子类给出
protected abstract PageMapper<T> getMapper();
//默认实现,无where条件
public Page<T> selectPage(PageRequest request){
PageMapper<T> pageMapper = getMapper();
List<T> list = pageMapper.findAll(request);
Long count = pageMapper.countAll();
return afterSelect(request.getSize(), list, count);
}
//在查询之后,创建page结果对象
protected Page<T> afterSelect(int size, List<T> list, long count){
Page<T> page = new Page<T>();
page.setRows(list);
page.setTotalPages((int) (count/size+1));
page.setTotalRows(count);
return page;
}
}
类似的与具体实体相关的地方,预留一个抽象方法,这里需要由子类给出具体的mapper接口。当初定义的PageMapper接口在这里起了作用,这里的分页方法,不需要去关心是哪个具体的mapper接口了,只需要关心怎么调用分页的两个dao层方法,去创建一个Page对象就行了。
在UserService中使用它。
/**
* Created by liuruijie on 2017/4/17.
* 用户相关接口
* ,继承PageService接口是为了获取到默认的分页实现
*/
public interface UserService extends PageService<UserInfo>{
//可自由扩展其他业务相关方法
···
}
这里继承PageService接口,因为在spring注入的时候,我们一般会使用接口类型的引用来指向具体的实例。如果这里不继承PageService接口,我们将无法获得selectPage分页方法。
然后是实现类
/**
* Created by liuruijie on 2017/4/17.
* 用户相关接口实现
* 继承PageServiceAdapter,获取默认分页实现
*/
@Service
public class UserServiceImpl extends PageServiceAdapter<UserInfo> implements UserService{
@Autowired
UserMapper userMapper;
//提供userMapper接口
@Override
protected PageMapper<UserInfo> getMapper() {
return userMapper;
}
//其他业务相关方法的实现
···
}
之前编写的PageServiceAdapter在这里使用,重写抽象方法getMapper,将具体的usermapper实例返回。
不需要写其他和分页相关的逻辑,写到这里就已经能够使用分页的默认实现了。
单元测试:
@Test
public void pageTest(){
//构建pageRequest对象,设置页码page和每页的记录数size。
PageRequest request = new PageRequest(1, 2);
//设置排序规则
request.setSorts(
new PageRequest.Sort[]{
new PageRequest.Sort("id","DESC")});
//得到page对象
Page<UserInfo> userInfoPage = userService.selectPage(request);
//序列化后输出
String json = JSON.toJSONString(userInfoPage);
System.out.println(json);
}
查询用户数据,页码为第1页,每页展示2条数据,按照id逆序排列。
执行结果:
{
"rows": [
{
"email": "1@1.1",
"id": 13,
"nickName": "AAAAA",
"passportId": "user2",
"phone": "11111111111"
},
{
"email": "1@1.1",
"id": 12,
"nickName": "AAAAA",
"passportId": "user1",
"phone": "12345678901"
}
],
"totalPages": 2,
"totalRows": 3
}
结果查出了两条记录,并且按id逆序排列,总页数为2,总记录数是3。测试无误。
总结:
首先要明确一点,没有绝对通用的工具,不可能存在能够解决所有业务的实现。而程序员能够做的,只是让代码尽可能地解耦,以分页这个例子来说,就是让具体的实体类,不用关心分页是怎样分的,让分页相关的逻辑,不用考虑具体是去哪张表查询,结果具体是放在哪个类里面。虽然这会让一个功能在最初的时候实现起来非常麻烦,但只要做出一些成果之后,想要扩展是很轻松的事情。
归纳一些小技巧:
1.泛型很有用,泛型能够让代码不用考虑类型。不仅是解耦的重要手段,还可以让你的代码看起来很高端/斜眼笑。
2.接口很有用,接口能够规范方法签名,面向接口可以让你在调用方法的时候,不用考虑具体的实现。不知不觉就降低了耦合度。
3.抽象类很有用,可以将某个功能对于不同业务的相同逻辑放到抽象类里面,而不同的部分以抽象方法的形式声明出来。子类必需实现抽象方法,以此来提供和具体业务相关的信息,但是子类不需要再去编写相同的部分。