no-image

支援連表查詢之類的複雜查詢的PageHelper修改

                                    

做APP,小程式就上開源眾包。世界盃期間免費領2600元啟動金。
>>>
  

先說說使用PageHelper踩過的坑:

在mapper對映檔案中,如果使用了limit關鍵字,而又使用了PageHelper的功能,比如orderBy,那麼就會報錯。
在使用稍微複雜的查詢時,PageHelper是針對最外層(最下方)的資料進行分頁的。

出現這樣的問題,歸根結底還是PageHelper原始碼的問題。

這套修改的原理即是修改pagehelper “limit”的位置,從末尾改到我們想要的位置。

不多說PageHelper本身,本文使用的PageHelper版本為

         <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>4.2.1</version>
        </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<String, Object> 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(*) &gt;= #{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 &gt; #{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<String, Dialect> urlDialectMap = new ConcurrentHashMap<String, Dialect>();    //快取
    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<String, Object> additionalParameters = (Map<String, Object>) 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<ParameterMapping> 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<ParameterMapping> allCountTempParameterMappingList = boundSql.getParameterMappings();
                    countTempParameterMappingList = new ArrayList<>();

                    for (Iterator<ParameterMapping> 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<ParameterMapping> 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<ParameterMapping> 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<ParameterMapping> 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             <!--指明資料庫 4.0.0以後不需要設定此屬性-->
10             <property name="dialect" value="mysql"/>
11             <!-- 該引數預設為false -->
12             <!-- 設定為true時,會將RowBounds第一個引數offset當成pageNum頁碼使用 -->
13             <!-- 和startPage中的pageNum效果一樣-->
14             <property name="offsetAsPageNum" value="true"/>
15             <!-- 該引數預設為false -->
16             <!-- 設定為true時,使用RowBounds分頁會進行count查詢 -->
17             <property name="rowBoundsWithCount" value="true"/>
18             <!-- 設定為true時,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果 -->
19             <!-- (相當於沒有執行分頁查詢,但是返回結果仍然是Page型別)-->
20             <property name="pageSizeZero" value="true"/>
21             <!-- 3.3.0版本可用 - 分頁引數合理化,預設false禁用 -->
22             <!-- 啟用合理化時,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最後一頁 -->
23             <!-- 禁用合理化時,如果pageNum<1或pageNum>pages會返回空資料 -->
24             <property name="reasonable" value="true"/>
25             <!-- 3.5.0版本可用 - 為了支援startPage(Object params)方法 -->
26             <!-- 增加了一個`params`引數來配置引數對映,用於從Map或ServletRequest中取值 -->
27             <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置對映的用預設值 -->
28             <!-- 不理解該含義的前提下,不要隨便複製該配置 -->
29             <property name="params" value="pageNum=start;pageSize=limit;"/>
30             <!-- 支援通過Mapper介面引數來傳遞分頁引數 -->
31             <property name="supportMethodsArguments" value="true"/>
32             <!-- always總是返回PageInfo型別,check檢查返回型別是否為PageInfo,none返回Page -->
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。

這是一種比較拙劣的方法!如果有更好的方法,歡迎指點指點~~


(adsbygoogle = window.adsbygoogle || []).push({});

function googleAdJSAtOnload() {
var element = document.createElement(“script”);
element.src = “//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js”;
element.async = true;
document.body.appendChild(element);
}
if (window.addEventListener) {
window.addEventListener(“load”, googleAdJSAtOnload, false);
} else if (window.attachEvent) {
window.attachEvent(“onload”, googleAdJSAtOnload);
} else {
window.onload = googleAdJSAtOnload;
}