网站建设企业的市场分析,淄博seo怎么选择,网站开发费是无形资产吗,短视频seo服务任何服务对数据库的日常操作#xff0c;都离不开增删改查。如果一次查询的纪录很多#xff0c;那我们必须采用分页的方式。对于一个Springboot项目#xff0c;访问和查询MySQL数据库#xff0c;持久化框架可以使用MyBatis#xff0c;分页工具可以使用github的 PageHelper。…任何服务对数据库的日常操作都离不开增删改查。如果一次查询的纪录很多那我们必须采用分页的方式。对于一个Springboot项目访问和查询MySQL数据库持久化框架可以使用MyBatis分页工具可以使用github的 PageHelper。我们来看一下PageHelper的使用方法
// 组装查询条件
ArticleVO articleVO new ArticleVO();
articleVO.setAuthor(张三);// 初始化返回类
// ResponsePages类是这样一种返回类其中包括返回代码code和返回消息msg
// 还包括返回的数据和分页信息
// 其中分页信息就是 com.github.pagehelper.Page? 类型
ResponsePagesListArticleVO responsePages new ResponsePages();// 这里为了简单写死分页参数。正确的做法是从查询条件中获取
// 假设需要获取第1页的数据每页20条记录
// com.github.pagehelper.Page? 类的基本字段如下
// pageNum: 当前页
// pageSize: 每页条数
// total: 总记录数
// pages: 总页数
com.github.pagehelper.Page? page PageHelper.startPage(1, 20);// 根据条件获取文章列表
ListArticleVO articleList articleMapper.getArticleListByCondition(articleVO);// 设置返回数据
responsePages.setData(articleList);// 设置分页信息
responsePages.setPage(page);如代码所示page 是组装好的分页参数即每页显示20条记录并且显示第1页。然后我们执行mapper的获取文章列表的方法返回了结果。此时我们查看 responsePages 的内容可以看到 articleList 中有20条记录page中包括当前页每页条数总记录数总页数等信息。
使用方法就是这么简单但是仅仅知道如何使用还不够还需要对原理有所了解。下面就来看看PageHelper 实现分页的原理。
我们先来看看 startPage 方法。进入此方法发现一堆方法重载最后进入真正的 startPage 方法有5个参数如下所示
/*** 开始分页** param pageNum 页码* param pageSize 每页显示数量* param count 是否进行count查询* param reasonable 分页合理化,null时用默认配置* param pageSizeZero true 且 pageSize0 时返回全部结果false时分页, null时用默认配置*/
public static E PageE startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {PageE page new PageE(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);// 当已经执行过orderBy的时候PageE oldPage SqlUtil.getLocalPage();if (oldPage ! null oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}SqlUtil.setLocalPage(page);return page;
}getLocalPage 和 setLocalPage 方法做了什么操作我们进入基类 BaseSqlUtil 看一下
package com.github.pagehelper.util;
...public class BaseSqlUtil {// 省略其他代码private static final ThreadLocalPage LOCAL_PAGE new ThreadLocalPage();/*** 从 ThreadLocalPage 中获取 page*/public static T PageT getLocalPage() {return LOCAL_PAGE.get();}/*** 将 page 设置到 ThreadLocalPage*/public static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}// 省略其他代码
}原来是将 page 放入了 ThreadLocal 中。ThreadLocal 是每个线程独有的变量与其他线程不影响是放置 page 的好地方。
setLocalPage 之后一定有地方 getLocalPage我们跟踪进入代码来看。
有了MyBatis动态代理的知识后我们知道最终执行SQL的地方是 MapperMethod 的 execute 方法作为回顾我们来看一下
package org.apache.ibatis.binding;
...public class MapperMethod {public Object execute(SqlSession sqlSession, Object[] args) {Object result;if (SqlCommandType.INSERT command.getType()) {// 省略} else if (SqlCommandType.UPDATE command.getType()) {// 省略} else if (SqlCommandType.DELETE command.getType()) {// 省略} else if (SqlCommandType.SELECT command.getType()) {if (method.returnsVoid() method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result null;} else if (method.returnsMany()) {/*** 获取多条记录*/result executeForMany(sqlSession, args);} else if ...// 省略} else if (SqlCommandType.FLUSH command.getType()) {// 省略} else {throw new BindingException(Unknown execution method for: command.getName());}...return result;}
}由于执行的是select操作并且需要查询多条纪录所以我们进入 executeForMany 这个方法中然后进入 selectList 方法然后是 executor.query 方法。再然后突然进入到了 mybatis 的 Plugin 类的 invoke 方法这是为什么
这里就必须提到 mybatis 提供的 Interceptor 接口。**Intercept 机制让我们可以将自己制作的分页插件 intercept 到查询语句执行的地方这是MyBatis对外提供的标准接口。**借助于Java的动态代理标准的拦截器可以拦截在指定的数据库访问流程中执行拦截器自定义的逻辑比如在执行SQL之前拦截拼装一个分页的SQL并执行。
让我们回到MyBatis初始化的时候我们发现 MyBatis 为我们组装了 sqlSessionFactory所有的 sqlSession 都是生成自这个 Factory。在这篇文章中我们将重点放在 interceptorChain 上。程序启动时MyBatis 或者是 mybatis-spring 会扫描代码中所有实现了 interceptor 接口的插件并将它们以【拦截器集合】的方式存储在 interceptorChain 中。如下所示
# sqlSessionFactory 中的重要信息sqlSessionFactoryconfigurationenvironment mapperRegistryconfig knownMappers mappedStatements resultMaps sqlFragments interceptorChain # MyBatis拦截器调用链interceptors # 拦截器集合记录了所有实现了Interceptor接口并且使用了invocation变量的类如果MyBatis检测到有拦截器它就会在拦截器指定的执行点首先执行 Plugin 的 invoke 方法唤醒拦截器然后执行拦截器定义的逻辑。因此当 query 方法即将执行的时候其实执行的是拦截器的逻辑。
MyBatis官网的说明
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下MyBatis 允许使用插件来拦截的方法调用包括
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)ParameterHandler (getParameterObject, setParameters)ResultSetHandler (handleResultSets, handleOutputParameters)StatementHandler (prepare, parameterize, batch, update, query)
如果想了解更多拦截器的知识可以看文末的参考资料。
我们回到主线继续看Plugin类的invoke方法
package org.apache.ibatis.plugin;
...public class Plugin implements InvocationHandler {...public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {SetMethod methods signatureMap.get(method.getDeclaringClass());if (methods ! null methods.contains(method)) {// 执行拦截器的逻辑return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}...
}我们去看 intercept 方法的实现这里我们进入【PageHelper】类来看
package com.github.pagehelper;
.../*** Mybatis - 通用分页拦截器*/
SuppressWarnings(rawtypes)
Intercepts(Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper extends BasePageHelper implements Interceptor {private final SqlUtil sqlUtil new SqlUtil();Overridepublic Object intercept(Invocation invocation) throws Throwable {// 执行 sqlUtil 的拦截逻辑return sqlUtil.intercept(invocation);}Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}Overridepublic void setProperties(Properties properties) {sqlUtil.setProperties(properties);}
}可以看到最终调用了 SqlUtil 的intercept 方法里面的 doIntercept 方法是 PageHelper 原理中最重要的方法。跟进来看
package com.github.pagehelper.util;
...public class SqlUtil extends BaseSqlUtil implements Constant {.../*** 真正的拦截器方法** param invocation* return* throws Throwable*/public Object intercept(Invocation invocation) throws Throwable {try {return doIntercept(invocation); // 执行拦截} finally {clearLocalPage(); // 清空 ThreadLocalPage}}/*** 真正的拦截器方法** param invocation* return* throws Throwable*/public Object doIntercept(Invocation invocation) throws Throwable {// 省略其他代码// 调用方法判断是否需要进行分页if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {ResultHandler resultHandler (ResultHandler) args[3];// 当前的目标对象Executor executor (Executor) invocation.getTarget();/*** getBoundSql 方法执行后boundSql 中保存的是没有 limit 的sql语句*/BoundSql boundSql ms.getBoundSql(parameterObject);// 反射获取动态参数MapString, Object additionalParameters (MapString, Object) additionalParametersField.get(boundSql);// 判断是否需要进行 count 查询默认需要if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {// 省略代码// 执行 count 查询Object countResultList executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);Long count (Long) ((List) countResultList).get(0);// 处理查询总数从 ThreadLocalPage 中取出 page 并设置 totalruntimeDialect.afterCount(count, parameterObject, rowBounds);if (count 0L) {// 当查询总数为 0 时直接返回空的结果return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);}}// 判断是否需要进行分页查询if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {/*** 生成分页的缓存 key* pageKey变量是分页参数存放的地方*/CacheKey pageKey executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);/*** 处理参数对象会从 ThreadLocalPage 中将分页参数取出来放入 pageKey 中* 主要逻辑就是这样代码就不再单独贴出来了有兴趣的同学可以跟进验证*/parameterObject runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);/*** 调用方言获取分页 sql* 该方法执行后pageSql中保存的sql语句被加上了 limit 语句*/String pageSql runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);BoundSql pageBoundSql new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);//设置动态参数for (String key : additionalParameters.keySet()) {pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));}/*** 执行分页查询*/resultList executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);} else {resultList new ArrayList();}} else {args[2] RowBounds.DEFAULT;// 不需要分页查询执行原方法不走代理resultList (List) invocation.proceed();}/*** 主要逻辑* 从 ThreadLocalPage 中取出 page* 将 resultList 塞进 page并返回*/return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);}...
}Count 查询语句 countBoundSql 被执行了分页查询语句 pageBoundSql 也被执行了。然后从 ThreadLocal 中将page 取出来设置记录总数每页条数等信息同时也将查询到的记录塞进page最后返回。再之后就是mybatis的常规后续操作了。
知识拓展
我们来看看 PageHelper 支持哪些数据库的分页操作
OracleMysqlMariaDBSQLiteHsqldbPostgreSQLDB2SqlServer(2005,2008)InformixH2SqlServer2012DerbyPhoenix
原来 PageHelper 支持这么多数据库那么持久化工具mybatis为什么不一口气把分页也做了呢
其实mybatis也有自带的分页方法 RowBounds。RowBounds简单地来说包括 offset 和 limit。实现原理是将所有符合条件的记录获取出来然后丢弃 offset 之前的数据只获取 limit 条数据。这种做法效率低下个人猜想mybatis只想把数据库连接和SQL执行这方面做精做强至于如分页之类的细节本身提供Intercept接口让第三方实现该接口来完成分页。PageHelper 就是这样的第三方分页插件。甚至你可以实现该接口制作你自己的业务逻辑拦截到任何MyBatis允许你拦截的地方。
总结
PageHelper 的分页原理最核心的部分是实现了 MyBatis 的 Interceptor 接口从而将分页参数拦截在执行sql之前拼装出分页sql到数据库中执行。
初始化的时候因为 PageHelper 的 SqlUtil 中实例化了 intercept 方法因此MyBatis 将它视作一个拦截器记录在 interceptorChain 中。
执行的时候PageHelper首先将 page 需求记录在 ThreadLocal Page 中然后在拦截的时候从 ThreadLocal Page 中取出 page拼装出分页sql然后执行。
同时将结果分页信息包括当前页每页条数总页数总记录数等设置回page让业务代码可以获取。