mybatis 框架虽然使用起来相当简单,且灵活,自己管理着 sql 语句,但是开发起来还是有着不少的工作量,比如在处理分页问题的时候,我们通常需要另外再查询一次总共的记录数
其实我们是希望把获取分页信息的工作给统一起来,简洁代码,减少工作量
我们可以利用 mybatis 的插件功能(拦截器)来处理分页,这里我们尝试着自己去写一个简单的分页插件,后面也有介绍一个优秀的 mybatis 分页插件 PageHelper 的使用方法
一、自己写一个简单的分页插件
在 mybatis xml 配置文件中,我们可以配置插件(plugins),mybatis 允许你在已映射语句执行过程中的某一点进行拦截调用,这样我们可以通过拦截查询方法,添加分页查询条件,包装查询结果
1、创建一个插件
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可
/* * 指定方法签名: * 下面的配置表面 将会拦截在 Executor 实例中所有的 “query” 方法调用, * 这里的 Executor 是负责执行低层映射语句的内部对象 */@Intercepts({@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class PageInterceptor implements Interceptor { /** * 拦截目标对象中目标方法的执行 */ @Override public Object intercept(Invocation invocation) throws Throwable { //TODO 覆盖目标方法,我们在这里覆盖目标方法 // 执行目标方法,并返回结果(这里没有处理) return invocation.proceed(); } /** * 包装目标对象,即为目标对象创建一个代理对象 */ @Override public Object plugin(Object target) { // 借助 Plugin 的 wrap(Object target,Interceptor interceptor); 包装我们的目标对象 // target: 目标对象, interceptor: 拦截器, this 表示使用当前拦截器 return Plugin.wrap(target, this); } /** * 获取插件注册时,传入的property属性 */ @Override public void setProperties(Properties properties) { // TODO }}
同时在 mybatis的配置文件中注册插件
上面的只是先创建了一个拦截器,还没有做具体的处理,现在我们先理清楚需要做的事情
- 拦截查询的目标方法,通过 Invocation 参数获取目标方法原来的参数信息
- 因为是分页查询,需要传入分页的参数条件,将通过是否传入有效的分页参数来判断是否要进行分页处理
- 除了要执行原本的查询语句,还要查询count总记录数,并计算出其他分页信息
- 将查询出来的结果和分页信息包装在一起并返回结果
2、创建接口和返回结果集
查询的时候,我们需要传入分页条件,这里我们创建一个 IPage 接口,为了规范统一
public interface IPage { /** * 当前页 */ Integer getPageNum(); /** * 每页数量 */ Integer getPageSize(); /** * 开始行 */ Integer getStartRow(); /** * 排序条件 */ String getOrderBy();}
并实现一个简单类 PageConfig.java
public class PageConfig implements IPage { private Integer pageNum; private Integer pageSize; private Integer startRow; private String orderBy; public PageConfig() { this(1,10); } public PageConfig(Integer pageNum,Integer pageSize) { this.pageNum = pageNum; this.pageSize = pageSize; this.startRow = (this.pageNum -1) * this.pageSize; } public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; this.startRow = (this.pageNum -1) * this.pageSize; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; this.startRow = (this.pageNum -1) * this.pageSize; } public String getOrderBy() { return orderBy; } public void setOrderBy(String orderBy) { this.orderBy = orderBy; } public Integer getStartRow() { return startRow; } public void setStartRow(Integer startRow) { this.startRow = startRow; }}
同时我们需要包装一下返回结果集,mybatis 直接查询返回的结果集类型为 ArrayList,我们要在此基础上加上分页信息
Page.javapublic class Pageextends ArrayList { private static final long serialVersionUID = 1L; private int pageNum; // 页码 private int pageSize; // 每页数量 private long total; // 总记录数 private int pages; // 页数 public Page() { super(); } public Page(int pageNum, int pageSize) { this(pageNum, pageSize, 0); } public Page(int pageNum, int pageSize, long total) { this.pageNum = pageNum; this.pageSize = pageSize; this.total = total; this.pages = (int)(this.pageSize == 0 ? 0 : this.total % this.pageSize == 0 ? this.total / this.pageSize : (this.total / this.pageSize + 1)); } public int getPageNum() { return pageNum; } public void setPageNum(int pageNum) { this.pageNum = pageNum; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public Long getTotal() { return total; } public void setTotal(long total) { this.total = total; this.pages = (int)(this.pageSize == 0 ? 0 : this.total % this.pageSize == 0 ? this.total / this.pageSize : (this.total / this.pageSize + 1)); } public int getPages() { return pages; } public void setPages(int pages) { this.pages = pages; } @Override public String toString() { return "Page [pageNum=" + pageNum + ", pageSize=" + pageSize + ", total=" + total + ", pages=" + pages + "]"; }}
3、处理查询方法
我们再顺一下拦截查询方法之后的流程
- 通过 Invocation 参数获取目标方法原来的参数信息
- 分析参数是否要进行分页处理,不需要不处理,执行原来的方法
- 添加分页条件,并查询结果(包括查询总数)
- 包装结果集并返回
这里直接附上实现代码
/* * 指定方法签名: 下面的配置表面 将会拦截在 Executor 实例中的 “query” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象 */@SuppressWarnings({"rawtypes", "unchecked"})@Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})public class PageInterceptor implements Interceptor { private final static String COUNT_SUFFIX = "_COUNT"; // count查询语句Id后缀 /** * 拦截目标对象中目标方法的执行 */ @Override public Object intercept(Invocation invocation) throws Throwable { // 获得目标对象 Executor executor = (Executor)invocation.getTarget(); // 获得目标方法的参数 Object[] args = invocation.getArgs(); // MappedStatement表示的是XML中的一个SQL MappedStatement mappedStatement = (MappedStatement)args[0]; Object parameter = args[1]; System.out.println(parameter.getClass()); RowBounds rowBounds = (RowBounds)args[2]; ResultHandler resultHandler = (ResultHandler)args[3]; BoundSql boundSql = mappedStatement.getBoundSql(parameter); CacheKey cacheKey = executor.createCacheKey(mappedStatement, parameter, rowBounds, boundSql); //获取分页参数 IPage pageConfig = getPageConfig(parameter); if (pageConfig != null) { return pageQuery(executor, mappedStatement, parameter, resultHandler, boundSql, cacheKey, pageConfig); } else { // 不用分页,执行目标方法 return invocation.proceed(); } } /** * 分页查询 * * @param executor * @param ms * @param parameter * @param resultHandler * @param boundSql * @param cacheKey * @param pageConfig * @return * @throws SQLException */ private Object pageQuery(Executor executor, MappedStatement ms, Object parameter, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey, IPage pageConfig) throws SQLException { Page page = new Page(pageConfig.getPageNum(), pageConfig.getPageSize()); // 查询总数 Long count = countQuery(executor, ms, parameter, resultHandler, boundSql); // 总数大于 0 ,开始分页查询 if (count != null && count > 0) { page.setTotal(count); // 生成分页的缓存 key CacheKey pageKey = cacheKey; // 获取分页 sql StringBuilder pageSql = new StringBuilder(boundSql.getSql()); if (pageConfig.getStartRow() == 0) { pageSql.append(" LIMIT ").append(pageConfig.getPageSize()); } else { pageSql.append(" LIMIT ").append(pageConfig.getStartRow()).append(",").append(pageConfig.getPageSize()); } BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql.toString(), boundSql.getParameterMappings(), parameter); List list = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); page.addAll(list); } return page; } /** * count 查询,获取总记录数 * * @param executor * @param mappedStatement * @param parameter * @param resultHandler * @param boundSql * @return * @throws SQLException */ private Long countQuery(Executor executor, MappedStatement mappedStatement, Object parameter, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 创建新的 MappedStatement 用于count查询 MappedStatement countMs = newCountMappedStatement(mappedStatement); // 创建 count 查询的缓存 key CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql); // 创建 countBoundSql String countSql = "SELECT COUNT(*) FROM (" + boundSql.getSql() + ") T"; BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter); // 执行 count 查询 Object countResult = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql); Long count = (Long)((List)countResult).get(0); return count; } /** * 新建用于count查询的MappedStatement * * @param ms * @return */ private MappedStatement newCountMappedStatement(MappedStatement ms) { String countMsId = ms.getId() + COUNT_SUFFIX; MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), countMsId, ms.getSqlSource(), ms.getSqlCommandType()); builder.resource(ms.getResource()); builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); builder.resultSetType(ms.getResultSetType()); builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); builder.timeout(ms.getTimeout()); builder.parameterMap(ms.getParameterMap()); if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) { StringBuilder keyProperties = new StringBuilder(); for (String keyProperty : ms.getKeyProperties()) { keyProperties.append(keyProperty).append(","); } keyProperties.delete(keyProperties.length() - 1, keyProperties.length()); builder.keyProperty(keyProperties.toString()); } // 设置返回结果为 Long ListresultMaps = new ArrayList (); ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, new ArrayList (0)) .build(); resultMaps.add(resultMap); builder.resultMaps(resultMaps); return builder.build(); } /** * 获取分页信息 * * @param parameter * @param rowBounds * @return */ private IPage getPageConfig(Object parameter) { IPage pageConfig = null; if (parameter instanceof IPage) { pageConfig = (IPage)parameter; }else if(parameter instanceof MapperMethod.ParamMap) { MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap)parameter; for(Object value:paramMap.values()) { if (value instanceof IPage) { pageConfig = (IPage)value; break; } } } return pageConfig; } /** * 包装目标对象,即为目标对象创建一个代理对象 */ @Override public Object plugin(Object target) { // 借助 Plugin 的 wrap(Object target,Interceptor interceptor); 包装我们的目标对象 // target: 目标对象, interceptor: 拦截器, this 表示使用当前拦截器 return Plugin.wrap(target, this); } /** * 获取插件注册时,传入的property属性 */ @Override public void setProperties(Properties properties) { }}
4、测试结果
要使用插件,记得在 mybatis xml 中配置plugin
我这个例子适用于使用 mapper 接口,只要接口方法有 IPage 的分页查询参数,就会分页查询
public interface UpmsUserMapper { ListselectUser(UpmsUser upmsUser, PageConfig pageConfig); List selectUser(IPage pageConfig);}
我们创建测试类测试一下结果
@RunWith(SpringJUnit4ClassRunner.class) //使用junit4进行测试 @ContextConfiguration ("/conf/spring/applicationContext*.xml") public class UpmsUserMapperTest { @Autowired private UpmsUserMapper upmsUserMapper; @Test public void testSelectUserUpmsUserPageConfig() { PageConfig pageConfig = new PageConfig(1, 2); Listlist = upmsUserMapper.selectUser(new UpmsUser(), pageConfig); System.out.println(list.getClass()); System.out.println(list.toString()); for (UpmsUser upmsUser : list) { System.out.println(upmsUser.toString()); } }}
测试结果如下
class com.brave.page.PagePage [pageNum=1, pageSize=2, total=3, pages=2][userId=10001][loginname=zou][password=123456][locked=null][userId=10002][loginname=zou][password=123456][locked=null]
二、分页插件 PageHelper 使用
上面的例子,只是简陋的实现一个分页插件,明白大概的流程。下面来介绍分页插件 PageHelper 的使用
PageHelper 的 github 地址
1、添加依赖
com.github.pagehelper pagehelper 5.1.8
由于使用了 sql 解析工具,还会引入 jsqlparser.jar 包,如果不是使用 maven,还需要自己导入 pagehelper 依赖版本一致的 jsqlparser.jar 包
2、配置拦截器插件
在 mybatis 配置文件中添加拦截器插件配置
如果是 spring 项目,也可以在 spring 配置文件的 SqlSessionFactoryBean 里面添加插件配置
helperDialect=mysql
两个地方的配置选其就好,不要同时生效
下面是具体分页插件参数
参数 | 描述 | 默认值 |
---|---|---|
helperDialect | 分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。该属性可以指定分页插件使用哪种方言。配置值:oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby | -- |
offsetAsPageNum | 该参数对使用 RowBounds 作为分页参数时有效。设为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页 | false |
rowBoundsWithCount | 该参数对使用 RowBounds 作为分页参数时有效。设置为true时,使用 RowBounds 分页会进行 count 查询 | false |
pageSizeZero | 设置为 true 时,如果 pageSize=0 或者 RowBounds.limit = 0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型) | false |
reasonable | 分页合理化参数。设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询 | false |
params | 为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值,可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero | |
supportMethodsArguments | 支持通过 Mapper 接口参数来传递分页参数,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest | false |
autoRuntimeDialect | 设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页 | false |
closeConn | 当使用运行时动态数据源或没有设置 helperDialect 属性自动获取数据库类型时,会自动获取一个数据库连接,通过该属性来设置是否关闭获取的这个连接,默认true关闭,设置为 false 后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定 | true |
aggregateFunctions(5.1.5+) | 默认为所有常见数据库的聚合函数,允许手动添加聚合函数(影响行数),所有以聚合函数开头的函数,在进行 count 转换时,会套一层。其他函数和列会被替换为 count(0),其中count列可以自己配置。 |
当 offsetAsPageNum=false 的时候,由于 PageNum 问题,RowBounds查询的时候 reasonable 会强制为 false。使用 PageHelper.startPage 方法不受影响。
这些参数是在默认情况(dialect)下有效,默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现 Dialect(com.github.pagehelper.Dialect) 接口,然后配置该属性为实现类的全限定名称。
3、使用方法
这里主要是4种使用方法
- RowBounds方式的调用
- PageHelper 的静态方法调用
- 接口方法使用分页参数
- ISelect 接口方式
1)RowBounds方式的调用
使用 RowBounds 参数进行分页,这种方式侵入性最小,用只是使用了这个参数,并没有增加其他任何内容
@Repositorypublic class UpmsUserDaoImpl extends SuperDao implements UpmsUserDao { @Override public ListselectUser(int offset,int limit) { List list = getSqlSession().selectList("com.brave.dao.UpmsUserDao2.selectUser", null, new RowBounds(offset, limit)); return list;}}
测试代码
@Testpublic void testSelectUser() { ListupmsUsers = upmsUserDao.selectUser(0, 10); for (UpmsUser upmsUser : upmsUsers) { System.out.println(upmsUser.toString()); }}
我们在配置插件的时候有两个参数是关于 RowBounds 的
- offsetAsPageNum 设置为 true 时,会将 RowBounds 中的 offset 参数当成 pageNum 使用,可以用页码和页面大小两个参数进行分页,也就是说上面的例子,我们查第一页时为
upmsUserDao.selectUser(1, 10)
- rowBoundsWithCount RowBounds 查询默认是没有 count 查询的,设置为true时,使用 RowBounds 分页会进行 count 查询
当 rowBoundsWithCount 设置为true时,会有 count 查询,会获得查询记录总数
@Testpublic void testSelectUser() { PageupmsUsers = (Page )upmsUserDao.selectUser(1, 10); System.out.println("总页数:"+upmsUsers.getTotal()); for (UpmsUser upmsUser : upmsUsers) { System.out.println(upmsUser.toString()); }}
没有设置或为false的话,total 为 -1
如果不设置 rowBoundsWithCount,也可以通过 PageRowBounds 进行 count 查询。
getSqlSession().selectList("com.brave.dao.UpmsUserDao.selectUser", null, new PageRowBounds (offset, limit))
PageRowBounds 继承 RowBounds,多了 total 属性
另外使用接口的时候也可以增加RowBounds参数
List<Country> selectAll(RowBounds rowBounds);
2)PageHelper 的静态方法调用
PageHelper 类有 startPage 和 offsetPage 方法去设置分页参数
在需要进行分页的 mybatis 查询方法前调用 PageHelper.startPage 静态方法即可,紧跟在这个方法后的第一个 mybatis 查询方法会被进行分页。
通常是在 service 的 bean 中调用 dao 的查询方法前 或者是 mapper 接口方法前 调用 PageHelper 的静态方法@Testpublic void testSelectUser2() { // request: url?pageNum=1&pageSize=10 // 支持 ServletRequest,Map,POJO 对象,需要配合 params 参数 // PageHelper.startPage(request); PageHelper.startPage(1, 10); PageupmsUsers = (Page )upmsUserDao.selectUser(); System.out.println("总页数:" + upmsUsers.getTotal()); for (UpmsUser upmsUser : upmsUsers) { System.out.println(upmsUser.toString()); }}
mapper 接口
@Servicepublic class UpmsUserServiceImpl implements UpmsUserService { @Autowired private UpmsUserMapper upmsUserMapper; @Override public ListlistUser(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); return upmsUserMapper.selectUser(); }}
使用 PageInfo
PageInfo 包含了非常全面的分页属性Listlist = (Page )upmsUserDao.selectUser();PageInfo pageInfo = new PageInfo(list);
private int pageNum;//当前页 private int pageSize;//每页的数量 private int size; //当前页的数量 //由于startRow和endRow不常用,这里说个具体的用法 //可以在页面中"显示startRow到endRow 共size条数据" private int startRow;//当前页面第一个元素在数据库中的行号 private int endRow;//当前页面最后一个元素在数据库中的行号 private int pages;//总页数 private int prePage;//前一页 private int nextPage;//下一页 private boolean isFirstPage = false;//是否为第一页 private boolean isLastPage = false;//是否为最后一页 private boolean hasPreviousPage = false;//是否有前一页 private boolean hasNextPage = false;//是否有下一页 private int navigatePages;//导航页码数 private int[] navigatepageNums;//所有导航页号 private int navigateFirstPage;//导航条上的第一页 private int navigateLastPage;//导航条上的最后一页
3)使用参数的方法
想要使用参数方式,需要配置 supportMethodsArguments 参数为 true,同时要配置 params 参数
可以在使用接口声明时就带上 pageNum 和 pageSize
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数public interface CountryMapper { ListselectByPageNumSize( @Param("upmsUser") UpmsUser upmsUser, @Param("pageNum") int pageNum, @Param("pageSize") int pageSize);}//配置supportMethodsArguments=true//在代码中直接调用:List list = countryMapper.selectByPageNumSize(user, 1, 10);
也可以把 pageNum 和 pageSize 存在于对象中,如 UpmsUser
//存在以下 Mapper 接口方法,不需要在 xml 处理后两个参数public interface CountryMapper { ListselectByPageNumSize(UpmsUser upmsUser);}//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页List list = countryMapper.selectByPageNumSize(upmsUser);
两种情况都需要 pageNum 和 pageSize 两个属性同时存在才会分页,且在 xml 中的sql中不需要处理这两个参数
4)ISelect 接口方式
另外还可以使用 ISelect 接口方式方式
Pagepage = PageHelper.startPage(1, 10).doSelectPage(new ISelect() { @Override public void doSelect() { upmsUserMapper.selectUser(); }});// lambda用法Page page = PageHelper.startPage(1, 10).doSelectPage(()-> upmsUserMapper.selectUser());//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPagepageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() { @Override public void doSelect() { upmsUserMapper.selectUser(); }});//对应的lambda用法pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> upmsUserMapper.selectUser());//count查询,返回一个查询语句的count数long total = PageHelper.count(new ISelect() { @Override public void doSelect() { upmsUserMapper.selectUser(country); }});//lambdatotal = PageHelper.count(()->upmsUserMapper.selectUser(country));
ISelect 接口方式除了可以保证安全外,还特别实现了将查询转换为单纯的 count 查询方式,这个方法可以将任意的查询方法,变成一个 select count(*) 的查询方法。
4、各使用方法的安全性考虑
- 使用 RowBounds 和 PageRowBounds 参数方式是极其安全的
- 使用参数方式是极其安全的
- 使用 ISelect 接口调用是极其安全的
不安全的时候:
PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。
下面这样的代码,就是不安全的用法:
//由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页PageHelper.startPage(1, 10);Listlist;if(param1 != null){ list = UpmsUserMapper.selectUser(param1);} else { list = new ArrayList ();}
我们只要把 PageHelper.startPage(1, 10);
方法放到if里面查询语句之前就好了