支持连表等复杂查询的PageHelper修改

新版本的PageHelper与mybatis请移步这里——支持PageHelper5.1.4 Mybatis3.4.6
先说说使用PageHelper踩过的坑:
- 在mapper映射文件中,如果使用了limit关键字,而又使用了PageHelper的功能,比如orderBy,那么就会报错。
- 在使用稍微复杂的查询时,PageHelper是针对最外层(最下方)的数据进行分页的。

出现这样的问题,归根结底还是PageHelper源码的问题。

这套修改的原理即是修改pagehelper “limit”的位置,从末尾改到我们想要的位置。

不多说PageHelper本身,本文使用的PageHelper版本为

         <dependency>
            <groupId>com.github.pagehelpergroupId>
            <artifactId>pagehelperartifactId>
            <version>4.2.1version>
        dependency>

这里以MySql为例,使得PageHelper支持复杂查询的分页。


一、添加CustomDialect,自定义方言

先看看原版的方言类的定义

package com.github.pagehelper.dialect;
    @Override
    public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        sqlBuilder.append(" limit ?,?");
        return sqlBuilder.toString();
    }

从这里可以看出,PageHelper在针对MySql数据库进行分页时,是在sql语句的最后加上limit偏移量进行分页查询的。且不说这种查询在数据量大时效率可能不高,这样去操作的话,在稍微复杂的查询中,通常无法正常的进行分页。

/**
 * Created by Anur IjuoKaruKas on 2017/9/28.
 * Description :
 */
public class CustomDialect extends MySqlDialect {

    public CustomDialect(SqlUtil sqlUtil) {
        super(sqlUtil);
    }

    @Override
    public Object processPageParameter(MappedStatement ms, Map paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        return super.processPageParameter(ms, paramMap, page, boundSql, pageKey);
    }

    @Override
    public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        if (sql.indexOf(ProjectConstant.SQL_SIGN) != -1) {
            StringBuffer stringBuffer = new StringBuffer(sql);
            stringBuffer.indexOf(ProjectConstant.SQL_SIGN);

            StringBuffer mae = new StringBuffer(stringBuffer.substring(0, stringBuffer.indexOf(ProjectConstant.SQL_SIGN)));
            StringBuffer uShiRo = new StringBuffer(stringBuffer.substring(stringBuffer.indexOf(ProjectConstant.SQL_SIGN), sql.length()));

            mae.insert(mae.lastIndexOf(")")," limit ?,?");
            return mae.append(uShiRo).toString();
        }
        sqlBuilder.append(sql);
        sqlBuilder.append(" limit ?,?");
        return sqlBuilder.toString();
    }
}

所以,在这里,我对它进行了一点点的改写。

新建一个CustomDialect继承MySqlDialect,并重写了原方法。

在这里,做了一个判断,如果sql语句中包含 * ProjectConstant.SQL_SIGN(limitable) ,那么在这前面加上 “limit ?,?”*

这个SQL_SIGN是什么呢?

public static final String SQL_SIGN = "AS limitable";

实际上就是一个用于标记哪里需要使用偏移量的一个标识。

在mapper映射文件中就可以这样写:

    <select id="selectRecordByComplexCondition" resultMap="ListQueryMap" parameterMap="ListQueryPo">
        SELECT
        *
        from
        (
            SELECT
            *
            FROM
            record r
            WHERE
            r.record_release_state = 1
            <if test="tagIdListToSqlString!=null">
                AND r.record_id IN (
                SELECT
                object_id
                FROM
                record_tag rtag
                WHERE
                rtag.tag_id IN ${tagIdListToSqlString}
                AND rtag.module = #{moduleEnumIndex}
                GROUP BY
                rtag.object_id
                HAVING
                count(*) >= #{tagCount}
                )
            if>
            AND r.module = #{moduleEnumIndex}
            <if test="rcId!=null">
                AND r.rc_id = #{rcId}
            if>
            <if test="raId!=null">
                AND r.ra_id = #{raId}
            if>
            <if test="recordTitle!=null">
                AND r.record_title LIKE CONCAT(CONCAT('%', #{recordTitle}), '%')
            if>
            <if test="recordReleaseTimeFrom!=null">
                AND r.record_release_time > #{recordReleaseTimeFrom}
            if>

    /* 现在的PageHelper limit 加在这里 */

            ) AS limitable
        LEFT JOIN record_tag rt ON rt.module = #{moduleEnumIndex}
        AND limitable.record_id = rt.object_id
        LEFT JOIN tag t ON rt.tag_id = t.tag_id

    /* 原来的PageHelper limit 加在这里 */
    select>

在这个业务中,我们想要进行分页的表是record,但如果不改mybatis,那么PageHelper将会对联表查询后的数据进行分页,那并不是我们想要的。


二、复写CustomPageHelper和CustomSqlUtil


/**
 * Created by Stack on 2017/9/28.
 * Description :
 */
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class CustomPageHelper extends PageHelper {
    private final CustomSqlUtil sqlUtil = new CustomSqlUtil();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return sqlUtil.intercept(invocation);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        sqlUtil.setProperties(properties);
    }
}
/**
 * Created by Anur IjuoKaruKas on 2017/9/28.
 * Description : 自定义sql处理工具
 */
public class CustomSqlUtil extends SqlUtil {
    private Dialect dialect;
    private Field additionalParametersField;
    private Properties properties;

    protected boolean autoDialect = true;    //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
    protected boolean autoRuntimeDialect;    //运行时自动获取dialect
    protected boolean closeConn = true;    //多数据源时,获取jdbcurl后是否关闭数据源
    private Map urlDialectMap = new ConcurrentHashMap();    //缓存
    private ReentrantLock lock = new ReentrantLock();

    /**
     * 真正的拦截器方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            return doIntercept(invocation);
        } finally {
            BaseSqlUtil.clearLocalPage();
        }
    }

    /**
     * 真正的拦截器方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object doIntercept(Invocation invocation) throws Throwable {
        //获取拦截方法的参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        List resultList;
        if (autoDialect) {
            lock.lock();
            try {
                if (autoDialect) {
                    autoDialect = false;
                    this.dialect = getDialect(ms);
                }
            } finally {
                lock.unlock();
            }
        }
        Dialect runtimeDialect = dialect;
        if (autoRuntimeDialect) {
            runtimeDialect = getDialect(ms);
        }
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {
            ResultHandler resultHandler = (ResultHandler) args[3];
            //当前的目标对象
            Executor executor = (Executor) invocation.getTarget();
            BoundSql boundSql = ms.getBoundSql(parameterObject);
            //反射获取动态参数
            Map additionalParameters = (Map) additionalParametersField.get(boundSql);
            //判断是否需要进行 count 查询
            if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {
                //创建 count 查询的缓存 key
                CacheKey countKey = executor.createCacheKey(ms, parameterObject, RowBounds.DEFAULT, boundSql);
                countKey.update("_Count");
                MappedStatement countMs = msCountMap.get(countKey);
                if (countMs == null) {
                    //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                    countMs = MSUtils.newCountMappedStatement(ms);
                    msCountMap.put(countKey, countMs);
                }
                //调用方言获取 count sql
                String countSql = runtimeDialect.getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);

                List countTempParameterMappingList = new ArrayList<>();

                //**************** 截取有效部分进行count操作 by Anur 17/10/25
                if (countSql.contains(BmsConstant.SQL_SIGN)) {
                    Integer subIndex = countSql.indexOf(BmsConstant.SQL_SIGN) + BmsConstant.SQL_SIGN.length();// 截取位置 到AS limitable
                    countSql = countSql.substring(0, subIndex);// 进行截取

                    Integer usefulParamCount = 0;// 参数舍弃
                    for (int i = 0; i < countSql.length(); i++) {
                        if (countSql.indexOf("?", i) != -1) {
                            usefulParamCount++;
                            i = countSql.indexOf("?", i);
                        }
                    }
                    List allCountTempParameterMappingList = boundSql.getParameterMappings();
                    countTempParameterMappingList = new ArrayList<>();

                    for (Iterator iterator = allCountTempParameterMappingList.iterator(); iterator.hasNext(); ) {
                        if (usefulParamCount == 0) {
                            break;
                        }
                        usefulParamCount--;
                        ParameterMapping next = iterator.next();
                        countTempParameterMappingList.add(next);
                    }
                } else {
                    countTempParameterMappingList = boundSql.getParameterMappings();
                }

                BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, countTempParameterMappingList, parameterObject);
                //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
                for (String key : additionalParameters.keySet()) {
                    countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }

                Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);// 执行 count 查询
                Long count = (Long) ((List) countResultList).get(0);// 处理查询总数
                runtimeDialect.afterCount(count, parameterObject, rowBounds);
                if (count == 0L) { // 当查询总数为 0 时,直接返回空的结果
                    return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);
                }
            }

            List pageTempParameterMappingList;
            // 判断是否需要进行分页查询
            if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {
                CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);// 生成分页的缓存 key
                parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);// 处理参数对象
                String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);// 调用方言获取分页 sql

                //**************** 参数调度 by Anur 17/10/25
                if (pageSql.contains(BmsConstant.SQL_SIGN)) {
                    String sqlInCount = pageSql.substring(0, pageSql.indexOf(BmsConstant.SQL_SIGN));
                    Integer count = -2;
                    for (int i = 0; i < sqlInCount.length(); i++) {
                        if (sqlInCount.indexOf("?", i) != -1) {
                            i = sqlInCount.indexOf("?", i);
                            count++;
                        }
                    }
                    List tempParameterMappingList = boundSql.getParameterMappings();
                    ParameterMapping parameterFirst = tempParameterMappingList.get(tempParameterMappingList.size() - 2);
                    ParameterMapping parameterSecond = tempParameterMappingList.get(tempParameterMappingList.size() - 1);
                    tempParameterMappingList.add(0 + count, parameterFirst);
                    tempParameterMappingList.add(1 + count, parameterSecond);
                    tempParameterMappingList.remove(tempParameterMappingList.size() - 2);
                    tempParameterMappingList.remove(tempParameterMappingList.size() - 1);

                    pageTempParameterMappingList = tempParameterMappingList;
                } else {
                    pageTempParameterMappingList = boundSql.getParameterMappings();
                }
                //****************

                BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, pageTempParameterMappingList, 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();
        }
        //返回默认查询
        return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);
    }

    /**
     * 初始化 dialect
     *
     * @param dialectClass
     * @param properties
     */
    private Dialect initDialect(String dialectClass, Properties properties) {
        Dialect dialect;
        try {
            dialect = new CustomDialect(this);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("初始化 dialect [" + dialectClass + "]时出错:" + e.getMessage());
        }
        dialect.setProperties(properties);
        return dialect;
    }

    /**
     * 获取url
     *
     * @param dataSource
     * @return
     */
    @Override
    public String getUrl(DataSource dataSource) {
        Connection conn = null;
        try {
            conn = dataSource.getConnection();
            return conn.getMetaData().getURL();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                try {
                    if (closeConn) {
                        conn.close();
                    }
                } catch (SQLException e) {
                    //ignore
                }
            }
        }
    }

    /**
     * 根据datasource创建对应的sqlUtil
     *
     * @param ms
     */
    @Override
    public Dialect getDialect(MappedStatement ms) {
        //改为对dataSource做缓存
        DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
        String url = getUrl(dataSource);
        if (urlDialectMap.containsKey(url)) {
            return urlDialectMap.get(url);
        }
        try {
            lock.lock();
            if (urlDialectMap.containsKey(url)) {
                return urlDialectMap.get(url);
            }
            if (StringUtil.isEmpty(url)) {
                throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
            }
            String dialectStr = BaseSqlUtil.fromJdbcUrl(url);
            if (dialectStr == null) {
                throw new RuntimeException("无法自动获取数据库类型,请通过 dialect 参数指定!");
            }
            Dialect dialect = initDialect(dialectStr, properties);
            urlDialectMap.put(url, dialect);
            return dialect;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void setProperties(Properties properties) {
        super.setProperties(properties);
        //多数据源时,获取jdbcurl后是否关闭数据源
        String closeConn = properties.getProperty("closeConn");
        //解决#97
        if (StringUtil.isNotEmpty(closeConn)) {
            this.closeConn = Boolean.parseBoolean(closeConn);
        }
        //数据库方言
        String dialect = properties.getProperty("dialect");
        String runtimeDialect = properties.getProperty("autoRuntimeDialect");
        if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
            this.autoRuntimeDialect = true;
            this.autoDialect = false;
            this.properties = properties;
        } else if (StringUtil.isEmpty(dialect)) {
            autoDialect = true;
            this.properties = properties;
        } else {
            autoDialect = false;
            this.dialect = initDialect(dialect, properties);
        }
        try {
            //反射获取 BoundSql 中的 additionalParameters 属性
            additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }
}

具体的原理就不详细说了,这里稍微分析了一点点。

                    // 参数调度
                if (pageSql.indexOf(ProjectConstant.SQL_SIGN) != -1) {
                    String sqlInCount = pageSql.substring(0, pageSql.indexOf(ProjectConstant.SQL_SIGN));
                    Integer count = 0;
                    for (int i = 0; i < sqlInCount.length(); i++) {
                        if (sqlInCount.indexOf("?", i) != -1) {
                            i = sqlInCount.indexOf("?", i);
                            count++;
                        }
                    }
                    List tempParameterMappingList = boundSql.getParameterMappings();
                    ParameterMapping parameterFirst = tempParameterMappingList.get(tempParameterMappingList.size() - 2);
                    ParameterMapping parameterSecond = tempParameterMappingList.get(tempParameterMappingList.size() - 1);
                    tempParameterMappingList.add(0 + count, parameterFirst);
                    tempParameterMappingList.add(1 + count, parameterSecond);
                    tempParameterMappingList.remove(tempParameterMappingList.size() - 2);
                    tempParameterMappingList.remove(tempParameterMappingList.size() - 1);

                    parameterMappingList = tempParameterMappingList;
                } else {
                    parameterMappingList = boundSql.getParameterMappings();
                }

基于PageHelper最大的改动是这里,在填充参数时,它的原本顺序是 ?,?,?,?,?,?,查询的位置,偏移量

但由于我们在前面吧 limit ?,? 移到了前面,所以这里也必须进行同步修改,实际上就是把这两个参数,根据sql语句的不同,提到前面去。

其他的也有一点点改动,比如说方言不再自动获取,直接使用我们上面自己定义的这个CustomDialect


三、配置

@Configuration
public class MybatisConfiguration {

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setTypeAliasesPackage(MODEL_PACKAGE);

        // 配置分页插件,详情请查阅官方文档
        CustomPageHelper customPageHelper = new CustomPageHelper();
        Properties properties = new Properties();
        properties.setProperty("pageSizeZero", "true");//分页尺寸为0时查询所有纪录不再执行分页
        properties.setProperty("reasonable", "true");//页码<=0 查询第一页,页码>=总页数查询最后一页
        properties.setProperty("supportMethodsArguments", "true");//支持通过 Mapper 接口参数来传递分页参数
        customPageHelper.setProperties(properties);

        // 添加插件
        factory.setPlugins(new Interceptor[]{customPageHelper});

        // 添加XML目录
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        return factory.getObject();
    }

直接沿用原来的配置就可以,只需对一个地方进行细微的修改,那就是在plugin这里,需要将pageHelper更换成自己定义的这个CustomPageHelper

 6 <configuration>
 7     <plugins>
 8         <plugin interceptor="com.xxxx.xxxxx.CustomPageHelper">
 9             
10             <property name="dialect" value="mysql"/>
11             
12             
13             
14             <property name="offsetAsPageNum" value="true"/>
15             
16             
17             <property name="rowBoundsWithCount" value="true"/>
18             
19             
20             <property name="pageSizeZero" value="true"/>
21             
22             
23             
24             <property name="reasonable" value="true"/>
25             
26             
27             
28             
29             <property name="params" value="pageNum=start;pageSize=limit;"/>
30             
31             <property name="supportMethodsArguments" value="true"/>
32             
33             <property name="returnPageInfo" value="check"/>
34         plugin>
35     plugins>
36 configuration>

如果配置文件是xml的,也是同样进行修改,将interceptor改成你自己定义的这个。


四、使用

 PageHelper.startPage(pageNum,pageSize);

和之前没有任何区别,你只需要在需要sql语句中需要进行偏移的地,做如下修改

需要分页的地方套上一层外衣 SELECT (xxxxxxxxxxxxx)AS limitable,就可以实现分页了

大功告成~ 这样的修改,在不会对正常的分页产生影响~


五、总结

不多说~ 这样可以根据自己的需求,随意对PageHelper进行修改,或者自己写一个低配版但是贴合需求的PageHelper。

这是一种比较拙劣的方法!如果有更好的方法,欢迎指点指点~~

你可能感兴趣的:(mybatis)