PageHelp 打印完整SQL

一、相关资源

官方资料

官方教程:https://pagehelper.github.io/docs/howtouse/
源码地址:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/README_zh.md
spring boot支持:https://github.com/pagehelper/pagehelper-spring-boot

使用注意,相关注意点:

https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Important.md

PageHelper.startPage`方法重要提示

只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会被分页。

请不要配置多个分页插件

请不要在系统中配置多个分页插件(使用Spring时,mybatis-config.xmlSpring<bean>配置方式,请选择其中一种,不要同时配置多个分页插件)!

分页插件不支持带有for update语句的分页

对于带有for update的sql,会抛出运行时异常,对于这样的sql建议手动分页,毕竟这样的sql需要重视。

分页插件不支持嵌套结果映射

由于嵌套结果方式会导致结果集被折叠,因此分页查询的结果在折叠后总数会减少,所以无法保证分页结果数量正确。

二、使用

1、简单使用

官方的如何使用:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
熟悉一个框架最基础的方式就是了解其使用,然后根据demo体验一下。
Demo 地址 :https://github.com/WangJi92/mybatis-log-demo
测试访问:http://127.0.0.1:7012/user/findAll

    @Override
    public List<UserDo> findAllUser() {
        PageHelper.offsetPage(10,10);
        UserDoExample example = new UserDoExample();
        example.createCriteria().andNameIsNotNull();
        return userDoMapper.selectByExample(example);
    }

2、原理

PageHelp 初体验+打印SQL 完整日志_java

结构图

PageHelp 初体验+打印SQL 完整日志_mybatis_02

步骤1

的情况,设置方法参数到ThreadLocal中去保存到com.github.pagehelper.page.PageMethod#LOCAL_PAGE 中去处理线。

步骤2

com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration#addPageInterceptor 中在mybaits 启动完成后处理org.apache.ibatis.session.SqlSessionFactory ,在mybaits 中配置添加拦截器

com.github.pagehelper.PageInterceptor 也就是这个进行了拦截

 @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

com/github/pagehelper/PageInterceptor.java:93 ,dialect 其实实现类就是com.github.pagehelper.PageHelper,根据线程中判断是否要进行参数拦截,然后处理一些会掉接口处理,dialect这个接口主要就是分页参数接口处理。 比如mysql的 com.github.pagehelper.dialect.helper.MySqlDialect

           //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);       

com.github.pagehelper.util.ExecutorUtil#pageQuery 这里代码很不错,作为参考修改mybatis 很有参考价值,比如租户隔离等等,产品隔离,动态的根据ThreadLocal 获取变量的信息,获取用户的信息等等。
BoundSql 是mybatis 中sql的一个简称,我们获取到最终的sql都是通过这个获取到的,这个参数中有参数映射、动态sql参数,等等,最终组装jdbc的java.sql.PreparedStatement 参数都是通过BoundSql 进行处理的。最终怎么组装成为sql 参考 org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters mybatis的DefaultParameterHandler,我自己也写了一个处理打印完整sql的 例子也是使用这个来实现的 因此这个非常的关键,这里的修改分页也是围绕着这个BoundSql的处理来展开的。org.apache.ibatis.executor.SimpleExecutor#doQuery 最终会交给 StatementHandler 去处理完成。

public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        //判断是否需要进行分页查询
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分页的缓存 key
            CacheKey pageKey = cacheKey;
            //处理参数对象
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //调用方言获取分页 sql
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //设置动态参数
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
    }

NOTE

由于分页插件使用ThreadLocal 进行处理一定会出现参数传递在线程池里面窜掉!一定要谨慎对待这个问题,由于很多的场景,出现异常或者退出没有关闭等等,比如MQ、RPC、JOB等这些调用使用ThreadLocal处理有时候处理没有及时的关闭清除导致窜掉。但是只要进行了拦截器之后都会自动清除掉com/github/pagehelper/PageInterceptor.java:113 afterAll 会清理。
官方:
PageHelp 初体验+打印SQL 完整日志_java_03

3、官方推荐安全使用

安全就是ThreadLocal窜掉的问题 。https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md 这里讲解了很多的

1、不安全的操作

PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

1.1 安全的写法

这里只要在执行到拦截器之前没有异常非常安全的!因为拦截器之后会进行清理工作。

List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种写法就能保证安全。
如果你对此不放心,你可以手动清理 ThreadLocal 存储的分页参数,可以像下面这样使用:

List<Country> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    try{
        list = countryMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<Country>();
}

这么写很不好看,而且没有必要。

2、推荐用法

方便简洁~

Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());

这种写法,如果第二个要使用分页就不是特别的清楚,因为这个是通过ThreadLocal 进行处理的。

//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
//紧跟着的第一个select方法会被分页
List<Country> list = countryMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分页时,实际返回的结果list类型是Page<E>,如果想取出分页信息,需要强制转换为Page<E>
assertEquals(182, ((Page) list).getTotal());

三、打印完整的SQL

1、问题

在看这个之前我自己也写了一个打印mybatis 完整sql的拦截 ,主要是拦截 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 然后通过 org/apache/ibatis/scripting/defaults/DefaultParameterHandler这个解析参数,最终能够达到打印完整sql的能力。 被一个同学提交了一个问题 https://github.com/WangJi92/mybatis-sql-log/issues/2 说是pagehelp不支持,就简单的写了一个dmeo来查看了一下这个问题。

2、原因

由于pagehelp和打印sql的这个拦截器是一个责任链模式 pagehelpInterceptor->logInterceptor ;pagehelpInterceptor直接执行完了 ,没有调用责任链中的 invocation.proceed() 导致后来的那个等他全部sql都执行完了,参数的那些信息也没有向下一个同类型的拦截器进行传递,因此打印的还是之前的老sql。

3、解决

我解决这个问题通过拦截StatementHandler,【语句处理器负责和JDBC层具体交互,包括prepare语句,执行语句,以及调用ParameterHandler.parameterize()设置参数】 完美的解决这个问题,因为pagehelp最后处理的BoundSql一定会在其中有这些参数哦。

效果图。
PageHelp 初体验+打印SQL 完整日志_pagehelp_04

四、总结

花了一点时间来了解一下pagehelp,感觉不错的,了解了一下也做了一下改进,对于这个也学习了一下!感觉还不错哦。
项目地址:https://github.com/WangJi92/mybatis-sql-log
DEMO地址:https://github.com/WangJi92/mybatis-log-demo