SpringBoot整合MyBatis分页
- 一、pagehelper分页
- 1、添加相关依赖
- 2、添加相关配置
- 3、添加分页配置
- 4、添加代码
- 5、测试
- 二、拦截器分页
- 1、添加相关配置
- 2、添加拦截器代码和配置
- 3、添加代码
- 4、测试
- 4、测试
本文目标: SpringBoot整合Mybatis分页的两种方式,一是利用PageHelper来实现,二是采用拦截器分页。
MyBatis框架分页实现方式:
- SQL分页,利用原生的sql关键字limit来实现(不推荐)
- 利用interceptor来拼接sql,实现和limit一样的功能(不推荐)
- 利用PageHelper来实现(简单)
- 拦截器分页(数据量大时,实现拦截器就很有必要了)
注意:分页的实现,是基于SpringBoot整合MyBatis 之上。
一、pagehelper分页
1、添加相关依赖
首先,我们需要在 pom.xml 文件中添加分页插件依赖包。
pom.xml
<!-- pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
2、添加相关配置
然后在 application.yml 配置文件中添加分页插件有关的配置。
application.yml
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
3、添加分页配置
分页查询请求封装类
PageRequest.java
import lombok.Data;
@Data
public class PageRequest {
// 当前页码
private int pageNum;
// 每页数量
private int pageSize;
}
分页查询结果封装类
PageResult.java
import lombok.Data;
import java.util.List;
@Data
public class PageResult {
// 当前页码
private int pageNum;
// 每页数量
private int pageSize;
// 记录总数
private long totalSize;
// 页码总数
private int totalPages;
// 数据模型
private List<?> content;
}
分页查询相关工具类。
PageUtils.java
import com.github.pagehelper.PageInfo;
public class PageUtils {
/**
* 将分页信息封装到统一的接口
* @param pageInfo
* @return
*/
public static PageResult getPageResult(PageInfo<?> pageInfo) {
PageResult pageResult = new PageResult();
pageResult.setPageNum(pageInfo.getPageNum());
pageResult.setPageSize(pageInfo.getPageSize());
pageResult.setTotalSize(pageInfo.getTotal());
pageResult.setTotalPages(pageInfo.getPages());
pageResult.setContent(pageInfo.getList());
return pageResult;
}
}
4、添加代码
UserMapper.xml
<!-- 查询分页 -->
<select id="getAllUserByPage" resultMap="BaseResultMap">
<include refid="Base_Column_List" />
from db_user
</select>
UserMapper.java
// 分页
List<User> getAllUserByPage();
service(IUserService.java && UserServiceImpl.java)
// IUserService
/**
* 分页查询接口
* 这里统一封装了分页请求和结果,避免直接引入具体框架的分页对象, 如MyBatis或JPA的分页对象
* 从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会
* 影响服务层以上的分页接口,起到了解耦的作用
* @param pageRequest 自定义,统一分页查询请求
* @return PageResult 自定义,统一分页查询结果
*/
PageResult getAllUserByPage(PageRequest pageRequest);
// UserServiceImpl
/** 分页查询 */
@Override
public PageResult getAllUserByPage(PageRequest pageRequest) {
return PageUtils.getPageResult(getPageInfo(pageRequest));
}
/**
* 调用分页插件完成分页
* @param pageRequest
* @return
*/
private PageInfo<User> getPageInfo(PageRequest pageRequest) {
int pageNum = pageRequest.getPageNum();
int pageSize = pageRequest.getPageSize();
PageHelper.startPage(pageNum, pageSize);
List<User> sysMenus = userMapper.getAllUserByPage();
return new PageInfo<User>(sysMenus);
}
controller(UserController.java)
// pagehelper 分页 post 请求
@PostMapping("/findPage")
public Result findPage(@RequestBody PageRequest pageQuery) {
return Result.success(userService.getAllUserByPage(pageQuery));
}
5、测试
测试工具:postman
测试路径:http://localhost:8081/findPage
结果图:
二、拦截器分页
实现原理主要是在数据库执行session查询的过程中,修改sql语句,先查询记录总条数,然后再分页查询数据记录,再把数据整合成分页数据形式就可以了。
1、添加相关配置
application-dev.yml
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
type-aliases-package: com.mmdz.entity
# sql 打印
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
config-location: classpath:mybatis/mybatis-config.xml
注意:要注释 configuration ,否则会报错
IllegalStateException: Property 'configuration' and 'configLocation' can not specified with together
。// 配置重复导致冲突 Caused by: java.lang.IllegalStateException: Property 'configuration' and 'configLocation' can not specified with together at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-5.3.1.jar:5.3.1] at org.mybatis.spring.SqlSessionFactoryBean.afterPropertiesSet(SqlSessionFactoryBean.java:488) ~[mybatis-spring-2.0.6.jar:2.0.6] at org.mybatis.spring.SqlSessionFactoryBean.getObject(SqlSessionFactoryBean.java:633) ~[mybatis-spring-2.0.6.jar:2.0.6]
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="cacheEnabled" value="false" /><!-- 暂时禁用缓存 -->
<setting name="logImpl" value="STDOUT_LOGGING"/><!-- 打印sql-->
</settings>
<plugins>
<plugin interceptor="com.mmdz.common.interceptor.PaginationInterceptor"></plugin>
</plugins>
</configuration>
2、添加拦截器代码和配置
PaginationInterceptor.java
**注意:**这个类拦截StatementHandler类的prepare方法,解析获取请求方法参数。如果是单个参数,且参数类型为SimplePage,则采用分页模式。或者有多个参数,并且其中一个参数被标注为page(即接口中@Param(“page”)),也启用分页模式。其中getCountSql(String sql)方法和getPageSql(String sql, SimplePage page)方法需要根据不同数据库进行修改,我用Mysql。
import com.mmdz.common.interceptor.page.SimplePage;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@Intercepts(
{ @Signature(type = StatementHandler.class,
method = "prepare", args = { Connection.class , Integer.class}) })
public class PaginationInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(PaginationInterceptor.class);
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
private static String dialect = "mysql";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获得拦截的对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 待执行的sql的包装对象
BoundSql boundSql = statementHandler.getBoundSql();
// 判断是否是查询语句
if (isSelect(boundSql.getSql())) {
// 获得参数集合
Object params = boundSql.getParameterObject();
if (params instanceof Map) { // 请求为多个参数,参数采用Map封装
return complexParamsHandler(invocation, boundSql, (Map<?, ?>) params);
} else if (params instanceof SimplePage) { // 单个参数且为Page,则表示该操作需要进行分页处理
return simpleParamHandler(invocation, boundSql, (SimplePage) params);
}
}
return invocation.proceed();
}
private Object complexParamsHandler(Invocation invocation, BoundSql boundSql, Map<?, ?> params) throws Throwable {
//判断参数中是否指定分页
if (containsPage(params)) {
return pageHandlerExecutor(invocation, boundSql, (SimplePage) params.get("page"));
} else {
return invocation.proceed();
}
}
private boolean containsPage(Map<?, ?> params) {
if(params==null){
return false;
}else if(!params.containsKey("page")){
return false;
}
Object page = params.get("page");
if(page==null){
return false;
}else if(page instanceof SimplePage){
return true;
}
return false;
}
private boolean isSelect(String sql) {
if (!StringUtils.isEmpty(sql) && sql.toUpperCase().trim().startsWith("SELECT")) {
return true;
}
return false;
}
private Object simpleParamHandler(Invocation invocation, BoundSql boundSql, SimplePage page) throws Throwable {
return pageHandlerExecutor(invocation, boundSql, page);
}
private Object pageHandlerExecutor(Invocation invocation, BoundSql boundSql, SimplePage page) throws Throwable {
// 获得数据库连接
Connection connection = (Connection) invocation.getArgs()[0];
// 使用Mybatis提供的MetaObject,该对象主要用于获取包装对象的属性值
MetaObject statementHandler = MetaObject.forObject(invocation.getTarget(), DEFAULT_OBJECT_FACTORY,
DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
// 获取该sql执行的结果集总数
int maxSize = getTotalSize(connection, (MappedStatement) statementHandler.getValue("delegate.mappedStatement"),
boundSql);
// 生成分页sql
page.setTotalRecord(maxSize);
String wrapperSql = getPageSql(boundSql.getSql(), page);
MetaObject boundSqlMeta = MetaObject.forObject(boundSql, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,
DEFAULT_REFLECTOR_FACTORY);
// 修改boundSql的sql
boundSqlMeta.setValue("sql", wrapperSql);
return invocation.proceed();
}
private int getTotalSize(Connection connection, MappedStatement mappedStatement, BoundSql boundSql) {
String countSql = getCountSql(boundSql.getSql());
PreparedStatement countStmt;
ResultSet rs;
List<AutoCloseable> closeableList = new ArrayList<AutoCloseable>();
try {
countStmt = connection.prepareStatement(countSql);
BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
boundSql.getParameterMappings(), boundSql.getParameterObject());
setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
rs = countStmt.executeQuery();
if (rs.next()) {
return rs.getInt(1);
}
closeableList.add(countStmt);
closeableList.add(rs);
} catch (SQLException e) {
logger.error("append an exception[{}] when execute sql[{}] with {}", e, countSql,
boundSql.getParameterObject());
} finally {
for (AutoCloseable closeable : closeableList) {
try {
if (closeable != null)
closeable.close();
} catch (Exception e) {
logger.error("append an exception[{}] when close resource[{}] ", e, closeable);
}
}
}
return 0;
}
private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
Object parameterObject) throws SQLException {
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler.setParameters(ps);
}
@Override
public Object plugin(Object target) {
// 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的次数
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
public String getCountSql(String sql) {
if("mysql".equals(dialect)){
return "select count(0) from (" + sql + ") as total";
}
return sql;
}
public String getPageSql(String sql, SimplePage page) {
if(page.getPage()<=0){
page.setPage(1);
}
if(page.getRows()<=0){
page.setRows(20);
}
int startRow = (page.getPage()-1)*page.getRows();
if(startRow>=page.getTotalRecord()){
page.setPage(1);
startRow=0;
}
if("mysql".equals(dialect)){
return sql+" limit "+startRow+", "+page.getRows();
}
return sql;
}
}
SimplePage.java
**注意:**这个类是分页对象所需的最简单的类,只要包含页码、每页条数、总条数和数据记录,就可以推算出所有需要的分页参数。所以用这个类来给拦截器做参数判断,若需要更多的页码信息可以重写一个分页类继承这个SimplePage即可。
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.List;
@Getter
@Setter
public class SimplePage implements Serializable {
protected static final long serialVersionUID = 5136213157391895517L;
protected int page = 1;// 页码,默认是第一页
protected int rows = 10;// 每页显示的记录数,默认是10
protected int totalRecord;// 总记录数
protected List data;// 当前页记录
public SimplePage setData(List data) {
this.data = data;
return this;
}
}
Page.java
**注意:**这个Page类主要是丰富SimplePage类,最重要的就是 setData(List data)方法,由这个方法来丰富一些变量数据。
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class Page extends SimplePage {
private static final long serialVersionUID = -6190845403265328029L;
private boolean isFirstPage = true;//是否是第一页
private boolean isLastPage = false;//是否是最后一页
private int pageCount = 0;//当前页总记录数
private int totalPage = 0;//总页数
private int prePage = 1;//上一页页码
private int nextPage = 1;//下一页页码
public Page() {
super();
}
public Page(int page, int rows) {
super();
setPage(page);
setRows(rows);
}
@Override
public Page setData(List data){
super.setData(data);
if(data!=null && data.size()>0){
pageCount = data.size();
if(this.page==1){
isFirstPage=true;
}else{
isFirstPage=false;
}
//***
totalPage = (int)Math.ceil(totalRecord/(double)rows);
//***
if(page==totalPage){
isLastPage = true;
}else{
isLastPage = false;
}
//***
if(isFirstPage){
prePage = 1;
}else{
prePage = page-1;
}
//***
if(isLastPage){
nextPage = 1;
}else{
nextPage = page+1;
}
}else{
isLastPage = true;
}
return this;
}
}
3、添加代码
UserMapper.xml
<select id="findPage" resultMap="BaseResultMap">
<include refid="Base_Column_List" />
from db_user
</select>
UserMapper.java
**注意:**如果这个查询方法只有分页参数page,没有别的参数,可以不写@Param(“page”),若有多个参数必须用@Param标明参数名,这是拦截器判断分页的依据。
/** @Param("page")是应分页插件要求编写的 */
List<User> findPage(@Param("page") Page page);
UserServiceImpl.java
/**
* 拦截器分页
* @param page
* @return
*/
@Override
public Page findPage(Page page) {
List<User> list = userMapper.findPage(page);
page.setData(list);
return page;
}
IUserService.java
/**
* 拦截器分页
* @param page
* @return
*/
Page findPage(Page page);
UserController.java
// 拦截器 分页 post 请求
@PostMapping("/findPage2")
public Result findPage(@RequestBody Page page){
return Result.success(userService.findPage(page));
}
4、测试
测试工具:postman
测试路径:http://localhost:8081/findPage2
结果图:
pl.java**
/**
* 拦截器分页
* @param page
* @return
*/
@Override
public Page findPage(Page page) {
List<User> list = userMapper.findPage(page);
page.setData(list);
return page;
}
IUserService.java
/**
* 拦截器分页
* @param page
* @return
*/
Page findPage(Page page);
UserController.java
// 拦截器 分页 post 请求
@PostMapping("/findPage2")
public Result findPage(@RequestBody Page page){
return Result.success(userService.findPage(page));
}
4、测试
测试工具:postman
测试路径:http://localhost:8081/findPage2
结果图: