一. 开辟筹办
. 开辟东西 2. 开辟情况 Red Hat Open JDK 8u256 Apache Maven 3.6.3
3. 开辟依靠 SpringBoot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
赶钙代码
MyBatis
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency> 赶钙代码
PageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency> 赶钙代码
两. 手艺文档
1. 基于SpringBoot SpringBoot 民圆文档 https://spring.io/projects/spring-boot SpringBoot 中文社区 https://springboot.io/
2. 基于MyBatis MyBatis 民圆文档 https://mybatis.org/mybatis-3/zh/index.html
3. 散成PageHelper PageHelper 开源堆栈 https://github.com/pagehelper/Mybatis-PageHelper
三. 使用解说
1. 根本利用 正在实践项目使用中,PageHelper的利用十分便当快速,仅经由过程PageInfo + PageHelper两个类,便足以完身分页功用,但是常常这类最简朴的散成利用方法,却正在许多实践使用场景中,出有获得充实的开辟操纵.
接下去是我们最多见的利用方法:
public PageInfo<ResponseEntityDto> page(RequestParamDto param) {
PageHelper.startPage(param.getPageNum(), param.getPageSize());
List<ResoinseEntityDto> list = mapper.selectManySelective(param);
PageInfo<ResponseEntityDto> pageInfo = (PageInfo<ResponseEntityDto>)list;
return pageInfo;
} 赶钙代码
正在某种水平上而行,沙脉写法确实识帖开PageHelper的利用标准 :
正在汇合查询前利用PageHelper.startPage(pageNum,pageSize),而且中心不克不及交叉施行其他SQL
可是做为Developer的我们,常常只要正在寻求完善战极致的门路上才气够觅得打破战机缘;
以下是公道且标准的根本利用:
public PageInfo<ResponseEntityDto> page(RequestParamDto param) {
return PageHelper.startPage(param.getPageNum(), param.getPageSize())
.doSelectPageInfo(() -> list(param))
}
public List<ResponseEntityDto> list(RequestParamDto param) {
return mapper.selectManySelective(param);
} 赶钙代码
FAQ 1. 为何要从头声明一个list函数? 问: 常常正在许多实践营业使用场景中, 分页查询是基于年夜数据量的表格展现需供去停止的.
但是许多时分,比如: 内部效劳的相互挪用,OpenAPI的供给.
以至正在钠舂前后端别离联调的营业场景中,是一样需求一个范讨页汇合查询接心去供给效劳的.
别的,临时以上身分扔开没有道,我们能够按照沙脉写法去界说战标准钠舂工具
比如: 分页战汇合查询的别离息争耦(解耦详情请看进阶利用),
分蚁珉供的恳求战呼应取实践营业参数的别离(详情请看进阶利用)等涤氕
2. doSelectPageInfo是甚么? 问: doSelectPageInfo是PageHelper.startPage()函数返回的默许Page真例内置的函数,该函数能够用以Lambda的情势经由过程分外的Function去停止查询而没有需求再停止过剩的PageInfo取List转换,而doSelectPageInfo的参数则是PageHelper内置的Function(ISelect)接心用以到达转换PageInfo的目标
3. 这类写法的代码量看起去很多反多? 问: 正好像①中所形貌的,便代码量而行,的确出有更进一步的简化,可是再钠舂营业场景中,正在已具有list函数接心的状况下,是一种更曲不雅的劣化(劣化详情请看进阶利用)
2. 进阶利用 先看代码,再道剖析:
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import java.util.List;
/**
* @param <Param> 泛型request
* @param <Result> 泛型response
*/
public interface BaseService<Param, Result> {
/**
* 分页查询
*
* @param param 恳求参数DTO
* @return 分页汇合
*/
default PageInfo<Result> page(PageParam<Param> param) {
return PageHelper.startPage(param).doSelectPageInfo(() -> list(param.getParam()));
}
/**
* 汇合查询
*
* @param param 查询参数
* @return 查询呼应
*/
List<Result> list(Param param);
} 赶钙代码
能够看到BaseService能够做为齐局Service通用接心的启拆战声明
而做为通用分页接心page函数却正在此处操纵interface特庸呢键字default 间接声清楚明了page函数的办法体body
import com.github.pagehelper.IPage;
import lombok.Data;
import lombok.experimental.Accessors;
@Data // 为省略冗余代码利用lombok 实践应有通例Getter/Setter Construction toString等
@Accessors(chain = true) // 此lombok注解是为了完成 Entity真Build 比如: entity.setX(x).setY(y)
public class PageParam<T> implements IPage {
// description = "页码", defaultValue = 1
private Integer pageNum = 1;
// description = "页数", defaultValue = 20
private Integer pageSize = 20;
// description = "排序", example = "id desc"
private String orderBy;
// description = "参数"
private T param;
public PageParam<T> setOrderBy(String orderBy) {
this.orderBy = orderBy; // 此处可劣化 劣化详情且看剖析
return this;
}
} 赶钙代码
正在BaseService中我们看到了一个新的PageParam,参考了PageInfo用以包拆/声明/别离分页参数战营业参数,且参数范例为泛型,枷抚持任何数据范例的营业参数
同时也能够看到PageParam完成了IPage接心,而且多了一个orderBy属性字段
import common.base.BaseService;
import dto.req.TemplateReqDto;
import dto.resp.TemplateRespDto;
public interface TemplateService extends BaseService<TemplateReqDto, TemplateeRespDto> {
// 同为interface接心, 营业Service只需求担当BaseService
// 并按照实践利用场景声明恳求参数战呼应成果的Entity真体便可
} 赶钙代码
正在实践使用中,只需求声明我们通用的营业查询恳求参数战呼应成果便可
import dto.req.TemplateReqDto;
import dto.resp.TemplateRespDto;
import service.TemplateService;
import persistence.mapper.TemplateMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j // 基于lombok主动天生logger日记记载真例
@Service // SpringBoot中注册Service Bean的注解
@RequiredArgsConstructor // 基于lombok按照类一切final属性天生机关函数 便可完成Spring机关注进
public class TemplateServiceImpl implements TemplateService {
private final TemplateMapper mapper;
@Override
public List<TemplateRespDto> list(TemplateReqDto param) {
return mapper.selectManySelective(param) // 可按照实践状况将真体做转换
}
}
赶钙代码
完成类中也只需求重写list办法体,将实践营业场景种硅要处置的营业逻辑处置战查询办法写进此中,其实不需求体贴分页功用
@Slf4j // 同上
@RestController // SpringBoot中注册Controller Bean的注解
@RequiredArgsConstructor // 同上
public class TemplateController {
public final TemplateService service;
/**
* 分页查询
*
* @param pageParam 分页查询参数
* @return 分页查询呼应
*/
@PostMapping(path = "page")
public PageInfo<Result> page(@RequestBody PageParam<Param> pageParam) {
return service.page(pageParam);
}
/**
* 汇合查询
*
* @param listParam 汇合查询参数
* @return 汇合查询呼应
*/
@PostMapping(path = "list")
public List<Result> list(@RequestBody Param listParam) {
return service.list(listParam);
}
}
赶钙代码
最初编码Controller接心时,也只需求间接挪用service.page便可,而恳求参数间接用PageParam包拆,将分页参数战营业参数别离,正在前后豆心联调中,连结这类别离标准,能够很年夜水平上的低落相同战开辟本钱
FAQ 1. BaseService做为interface,page为何能够声明办法体? 问: Java8种孤特征之一便是为interface接心类增长了static/default办法,即声明办法后,其子类或完成皆将默许具有那些办法,能够间接挪用
而正在此处为Page办法声明default是由于page函数纸柝注分页参数战分沂奠应,离开了营业场景,办法体截然不同,以是干脆笼统界说出去,免除了实在现的庞大冗余历程
2. PageParam的声明有甚么意义?完成IPage是为了甚么? 问: PageParam是参考PageInfo编写的类(没有肯定今后PageHelper能否会启拆词攀类,也许我能够提个Issue上来,也到场开栽域架的开辟)
编写词攀类的目标便是为了别离分页战营业数据,闪开收者专注于营业的完成战开辟,同时也是对分页查询API的一中墟范,不管是恳求仍是呼应皆将分沂掂闭的数据抽离出去,零丁利用
而完成IPage则是由于IPage做为PageHelper内置的interface,正在没有理解它更多意义上的感化前,能够做为我枚讨页参数声明的一中墟范,而IPage中也只声清楚明了三个办法,别离是pageNum/pageSize/orderBy的Getter办法,别的正在源码阐发中,我将会提到完成垂心更深层的意义
3. PageParam中除通例的pageNum/pageSize,为何借需求一个orderBy? 问: 通例的分页查询中只需求pageNum/pageSize便可完身分页的目标,可是常常陪伴着分页查询的另有挑选排序,而orderBy则是专注基于SQL的静态传参排序
4. orderBy怎样利用?会有甚么成绩吗? 问: orderBy战pageNum/pageSize一样,皆是Pagehelper经由过程MyBatis阻拦器,正在query查询中注进出来的,以是正在前端传参时,orderBy参数应为数据库column desc/asc这类情势,多字段排序则能够用逗号(,)拼接,比如: columnA desc,columnB,
可是别的一圆里又存正在两个成绩, 第一便是年夜大都数据库表字段设想中,城市利用蛇形case定名,而十分规开辟中的驼峰case定名,以是存正在一层转换,而这类转换能够分派给前端传参时,也能够分派给后豆参时.
第两便是如许赤裸裸的将排序字墩姗露正在接心中,会存正在order by SQL注进的风险,以是正在实践利用过程当中,我们需求经由过程钠舂手腕来校验战排查orderBy的传参能否正当,比如用正则表达式婚配参数值只能露有order by语法中须要的值,比方字段名,desc or asc,没有许可包罗特别字符/数据库枢纽字等
5. pageNum/pageSize必然需求给默许值吗? 问: 经由过程浏览PageHelper源码,我们得知正在Page查询参数为null时,它其实不会付与它们默许值,其实不停止分外的处置,以致于招致分页失利,而给默许值,也是为凉防前后兜厉试接心过程当中能够会呈现的各类不测
3. 源码阐发
起首我们看PageHelper.startPage(param)过程当中发作了甚么 :
public static <E> Page<E> startPage(Object params) {
Page<E> page = PageObjectUtil.getPageFromObject(params, true);
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
} 赶钙代码
那是PageHelper担当(extend)的笼统类PageMethod中的一个静态办法
再看代码第一止 Page<E> page = PageObjectUtil.getPageFromObject(params, true)发作了甚么:
public static <T> Page<T> getPageFromObject(Object params, boolean required) {
if (params == null) {
throw new PageException("没法获得分页查询参数!");
} else if (params instanceof IPage) {
IPage pageParams = (IPage)params;
Page page = null;
if (pageParams.getPageNum() != null && pageParams.getPageSize() != null) {
page = new Page(pageParams.getPageNum(), pageParams.getPageSize());
}
if (StringUtil.isNotEmpty(pageParams.getOrderBy())) {
if (page != null) {
page.setOrderBy(pageParams.getOrderBy());
} else {
page = new Page();
page.setOrderBy(pageParams.getOrderBy());
page.setOrderByOnly(true);
}
}
return page;
} else {
... // 此处我只截与了部门代码片断, 以沙虑较为主要的一块
}
} 赶钙代码
能够看到正在此办法中,会先判定params能否为null,再而经由过程instanceof判定能否为IPage的子类或完成类
假如以上两个if/else 皆没有满意,则PageHelper则会正在我省略揭出的代码中经由过程大批的反射代码去获得pageNum/pageSize和orderBy.
总所皆知,反射正在Java中固然普遍使用,而且做为言语独占特征之一,深受广阔开辟者当辈爱,可是反射正在某种水平上,是需求机能本钱的,以至于现阶段许多支流的框架战手艺,皆正在只管削减反射的使用,以避免框架机能过好,被市场裁减.
那末到此为行,我们也终究注释并明白了为何PageParam要完成IPage接心了,正在此处的代码中能够间接经由过程接心获得到分页参数,而没有需求经由过程有益机能的反射获得PageHelper需求的参数
持续看startPage中的后绝代码:
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
protected static boolean DEFAULT_COUNT = true;
public PageMethod() {
}
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
public static <T> Page<T> getLocalPage() {
return (Page)LOCAL_PAGE.get();
}
...
...
} 赶钙代码
能够看到PageHelper担当的笼统类PageMethod中声清楚明了一个Page当边程当地变量,而getLocalPage()则是为了获得当火线程中的Page
而接下去if (oldPage != null && oldPage.isOrderByOnly())则是判定能否存正在旧分页数据
此处的isOrderByOnly经由过程getPageFromObject()函数我们能够明白,当只存正在orderBy参数时,即为true
也便是道,当存正在旧分页数据而且旧分页数据只要排序参数时,便将旧分页数据的排序参数列进新分页数据的排序参数
然后将新的分页数据page存进当地线程变量中
实践使用场景中,这类状况仍是比力少,仅排序而没有分页,以是某种角度上而行,我们仅当理解便好
接下去再看doSelectPageInfo(ISelect) 中发作了甚么:
public <E> PageInfo<E> doSelectPageInfo(ISelect select) {
select.doSelect();
return this.toPageInfo();
} 赶钙代码
能够看到,该办法的完成十分简朴清楚明了,便是经由过程注册声明ISelect接心由开辟捉义汇合查询方法并由它内部施行,随后便返回PageInfo真体
前里我们有提到,PageHelper基于MyBatis阻拦器到达分页的目标,那末为何此处的ISelect.doSelect()施行,就能够返回PageInfo真体呢?
实践上那即是阻拦器的妙用地点,正在select.doSelect()施行时,会触收PageHelper捉义的MyBatis查询阻拦器,并经由过程剖析SQL战SQL参数,按照数据库范例,停止分页,比如MySQL的limit,Oracle的Rownum等,
同时借会正在我们界说的查询SQL之前,PageHelper会从头天生一条select count(*)的SQL领先施行,已到达它界说Page内置分页参数的目标
@Intercepts({@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class PageInterceptor implements Interceptor {
private volatile Dialect dialect;
private String countSuffix = "_COUNT";
protected Cache<String, MappedStatement> msCountMap = null;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
public PageInterceptor() {
}
public Object intercept(Invocation invocation) throws Throwable {
...
...
}
} 赶钙代码
以上即是PageHelper内置的捉义MyBatis阻拦器,果代码量过量,为了包管没有违背本专文文不合错误题的准绳,此处没有再做过剩解说,若有需求,我能够另止写一篇专客零丁注释并解说MyBatis阻拦器的观点战道理,深度剖析MyBatis源码
拓展 PageHelper不只有pageNum/pageSize/orderBy那寂参数,更另有pageSizeZero, reasonable参数等用以更进阶的分页查询界说,如需更深化的理解,我能够另止写一遍进阶PageHelper利用,此文只做为平常开辟利用解说
四. 总结 PageHelper做为GitHub上如今远10K的开源分页架,或许代码深度战广度没有及支流市场框架战手艺,固然正在功用的完成战道理上,制轮子的易度没有下,源码也很明晰,可是正在很年夜水平上处理了许多基于MyBatis的分页手艺困难,简化并提醒了广阔开辟者的服从,那才是开辟者玫邻开辟的路上该当神驰并为之拼搏的标的目的战门路.
而我们做为受益者,也不该当仅仅是对其停止根本的利用,开辟之余,我们也该当存眷一些框架的拓展,对框架的蹬鲢有必然水平上的理解,并为之拓展战劣化