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.抽象类很有用,可以将某个功能对于不同业务的相同逻辑放到抽象类里面,而不同的部分以抽象方法的形式声明出来。子类必需实现抽象方法,以此来提供和具体业务相关的信息,但是子类不需要再去编写相同的部分。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容