写功能,即使这个功能很简单,也要考虑可扩展性。
最好的学习途径是从具体到抽象,最好的设计思路是从抽象到具体。
一.抽象输入输出
对于一个功能,使用者只需要关心输入和输出即可。
分页查询
输入:
页码和每页记录数(必填)
排序规则(非必需)
查询条件(非必需)
输出:
记录集合
记录总数
总页数
由此,能够设计与寻呼操作输入和输出对应的两个类
PageReqeust :
//*
*创建by liurui Jie on 2017/4/6。
*页面查询的参数封装
*/
公共类页面请求{
私密int size; //查询记录数
私有软件[ ] sorts; //排序
私密int start; //开始位置
//page是页码
publicpagerequest(intpage,int size,Sort. sorts )。
this.size=size;
this.sorts=sorts;
this.start=(page-1 ) *size;
}
公共静态类sort {
私有字符串类型;
私有字符串字段;
公共字段{
this.field=字段;
类型=' ASC ';
}
publicsort (字符串字段,字符串类型) {
this.field=字段;
this.type=type;
}
}
省略//set和get方法
}
此类封装了页面查询的页码、页面大小和排序规则。 因为查询条件必须由特定实体和特定业务决定,所以不方便在此封装。
Page :
//*
*创建by liurui Jie on 2017/4/6。
*页面查询返回值
*/
公共类页{
私密长总体rows; //总记录数
私有内总页面; //总页数
私有列表rows; //查过的记录
省略//set和get方法
}
此类封装分页查询所需的结果。
现在已经定义了分页的输入和输出,需要实现进程。
二、定义接口规格
同样,首先抽象分页操作
//*
*创建by liurui Jie on 2017/4/17。
*提供基本的分页接口
*/
公共接口页面服务{
pageselectpage (pagerequestrequest;
}
的分页(无查询条件)可以用这种方法进行。
已定义分页的顶级接口。 接下来设计刁钻层。
使用mybatis的注释方式,不用在xml中写sql的映射,只要直接在方法中写注释并给出sql即可。
还是先显示一下接口的规格。
//*
*创建by liurui Jie on 2017/4/17。
*对于需要分页sql映射的映射器
给予规范
*/
公共接口页面映射器{
列表查找所有(@ param (' page ) ) page请求请求);
长计数全部(;
}
两种方法,一种查数据,一种查数量。
三、提供缺省实现
本文不详细说明mybatis的使用。 有关具体使用,请参阅官方文档。
mybatis的注释方法映射sql,使用方法位于底部。
ww.my batis.org/my batis-3/zh/Java-API.html
然后,首先不要着急写具体的映射器接口,采用mybatis @SelectProvider注释绑定查询,写寻呼的提供器。
//*
* C
reated 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
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{
String tableName = "sys_user";
@SelectProvider(type = UserSqlProvider.class, method = "findAll")
List 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 implements PageService {
//此mapper由子类给出
protected abstract PageMapper getMapper();
//默认实现,无where条件
public Page selectPage(PageRequest request){
PageMapper pageMapper = getMapper();
List list = pageMapper.findAll(request);
Long count = pageMapper.countAll();
return afterSelect(request.getSize(), list, count);
}
//在查询之后,创建page结果对象
protected Page afterSelect(int size, List list, long count){
Page page = new Page();
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{
//可自由扩展其他业务相关方法
···
}
这里继承PageService接口,因为在spring注入的时候,我们一般会使用接口类型的引用来指向具体的实例。如果这里不继承PageService接口,我们将无法获得selectPage分页方法。
然后是实现类
/**
* Created by liuruijie on 2017/4/17.
* 用户相关接口实现
* 继承PageServiceAdapter,获取默认分页实现
*/
@Service
public class UserServiceImpl extends PageServiceAdapter implements UserService{
@Autowired
UserMapper userMapper;
//提供userMapper接口
@Override
protected PageMapper 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 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.抽象类很有用,可以将某个功能对于不同业务的相同逻辑放到抽象类里面,而不同的部分以抽象方法的形式声明出来。子类必需实现抽象方法,以此来提供和具体业务相关的信息,但是子类不需要再去编写相同的部分。