<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Mybatis分頁插件: pageHelper的使用及其原理解析

          共 32640字,需瀏覽 66分鐘

           ·

          2021-01-17 13:22

          走過路過不要錯(cuò)過

          點(diǎn)擊藍(lán)字關(guān)注我們


          在實(shí)際工作中,很進(jìn)行列表查詢的場(chǎng)景,我們往往都需要做兩個(gè)步驟:1. 查詢所需頁數(shù)對(duì)應(yīng)數(shù)據(jù);2. 統(tǒng)計(jì)符合條件的數(shù)據(jù)總數(shù);而這,又會(huì)導(dǎo)致我們必然至少要寫2個(gè)sql進(jìn)行操作。這無形中增加了我們的工作量,另外,當(dāng)發(fā)生需要變動(dòng)時(shí),我們又需要同時(shí)改動(dòng)這兩個(gè)sql,否則必然導(dǎo)致結(jié)果的不一致。

          因此,我們需要一個(gè)簡(jiǎn)單易用的分頁工具來幫我們完成這個(gè)工作了,需求明確,至于如何實(shí)現(xiàn)則各有千秋。而我們要說的 pageHelper則是這其中實(shí)現(xiàn)比較好的一件的組件了,我們就一起來看看如何使用它進(jìn)行提升工作效率吧!

          1. pageHelper 的依賴引入

          pom.xml 中引入pageHelper依賴:

          1. 如果是springboot, 則可以直接引入 pagehelper-spring-boot-starter, 它會(huì)幫我們省去許多不必要的配置。

                              com.github.pagehelper            pagehelper-spring-boot-starter            1.2.12        

          2. 如果是普通的springmvc 類的項(xiàng)目,則引入 pageHelper 即可。

                            com.github.pagehelper          pagehelper          5.1.10        

          2. pagehelper插件配置

          1. 如果是springboot,則直接配置幾個(gè)配置項(xiàng)即可:

          # mybatis 相關(guān)配置mybatis:  #... 其他配置信息  configuration-properties:    offsetAsPageNum: true    rowBoundsWithCount: true    reasonable: true  mapper-locations: mybatis/mapper/*.xml

          簡(jiǎn)單回顧看下db配置:

          # db 配置spring:  datasource:    driver-class-name: com.mysql.cj.jdbc.Driver    username: root    password: 123    url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&charactorEncoding=utf8&&serverTimezone=Asia/Shanghai

          2. 普通springmvc項(xiàng)目配置:mybatis-config.xml

              PUBLIC "-//mybatis.org//DTD Config 3.0//EN"    "http://mybatis.org/dtd/mybatis-3-config.dtd">

          并在配置數(shù)據(jù)源的時(shí)候,將mybatis配置文件指向以上文件。

          3. pagehelper 的使用

          使用的時(shí)候,只需在查詢list前,調(diào)用 startPage 設(shè)置分頁信息,即可使用分頁功能。

          public Object getUsers(int pageNum, int pageSize) {        PageHelper.startPage(pageNum, pageSize);        // 不帶分頁的查詢        List list = userMapper.selectAllWithPage(null);        // 可以將結(jié)果轉(zhuǎn)換為 Page , 然后獲取 count 和其他結(jié)果值        com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;        System.out.println("listCnt:" + listWithPage.getTotal());        return list;    }

          即使用時(shí), 只需提前聲明要分頁的信息, 得到的結(jié)果就是有分頁信息的了. 如果不想進(jìn)行count, 只要查分頁數(shù)據(jù), 則調(diào)用: PageHelper.startPage(pageNum, pageSize, false); 即可, 避免了不必要的count消耗.

          ?4. pageHelper 實(shí)現(xiàn)原理1: interceptor

          mybatis 有個(gè)插件機(jī)制,可以支持外部應(yīng)用進(jìn)行任意擴(kuò)展。它在啟動(dòng)的時(shí)候會(huì)將 interceptor 添加到mybatis的上下文中。然后在進(jìn)行查詢時(shí)再觸發(fā)實(shí)例化動(dòng)作.

          4.1 springboot 中接入interceptor

          springboot 中接入pagehelper非常簡(jiǎn)單, 主要受益于初始化的方式, 它會(huì)自動(dòng)加載配置.

            // com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration#addPageInterceptor    @PostConstruct    public void addPageInterceptor() {        // 初始化 com.github.pagehelper.PageInterceptor        PageInterceptor interceptor = new PageInterceptor();        Properties properties = new Properties();        //先把一般方式配置的屬性放進(jìn)去        properties.putAll(pageHelperProperties());        //在把特殊配置放進(jìn)去,由于close-conn 利用上面方式時(shí),屬性名就是 close-conn 而不是 closeConn,所以需要額外的一步        properties.putAll(this.properties.getProperties());        interceptor.setProperties(properties);        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {            // 添加inteceptor到 mybatis 中            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);        }    }  // org.apache.ibatis.session.Configuration#addInterceptor  public void addInterceptor(Interceptor interceptor) {    interceptorChain.addInterceptor(interceptor);  }
          // org.apache.ibatis.plugin.InterceptorChain#addInterceptor public void addInterceptor(Interceptor interceptor) { // 使用 ArrayList 保存intceptor interceptors.add(interceptor); }

          借助springboot的自動(dòng)配置, 獲取mybatis的sqlSessionFactoryList, 依次將 pagehelper 接入其中。

          4.2 interceptor的初始化

          將 interceptor 添加到mybatis上下文后, 會(huì)在每次調(diào)用查詢時(shí)進(jìn)行攔截請(qǐng)求, 它的初始化也會(huì)在這時(shí)候觸發(fā).

          // org.apache.ibatis.session.Configuration#newExecutor  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {    executorType = executorType == null ? defaultExecutorType : executorType;    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;    Executor executor;    if (ExecutorType.BATCH == executorType) {      executor = new BatchExecutor(this, transaction);    } else if (ExecutorType.REUSE == executorType) {      executor = new ReuseExecutor(this, transaction);    } else {      executor = new SimpleExecutor(this, transaction);    }    if (cacheEnabled) {      executor = new CachingExecutor(executor);    }    // 以interceptorChain包裝 executor, 以便inteceptor發(fā)揮作用    executor = (Executor) interceptorChain.pluginAll(executor);    return executor;  }
          // org.apache.ibatis.plugin.InterceptorChain#pluginAll public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { // 使用plugin一層層包裝 target, 具體實(shí)現(xiàn)為使用代理包裝 target // 所以, interceptor 的使用順序是按照添加的順序來的, 并不能自行設(shè)置 target = interceptor.plugin(target); } return target; }
          // com.github.pagehelper.PageInterceptor#plugin @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } // org.apache.ibatis.plugin.Plugin#wrap public static Object wrap(Object target, Interceptor interceptor) { // 獲取注解中說明的方式列表 @Intercepts -> @Signature, 下面我們看 pageInterceptor的注解 Map, Set> signatureMap = getSignatureMap(interceptor); Class type = target.getClass(); // 過濾需要進(jìn)行代理的接口, 而非全部代理 Class[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 使用jdk方式生成動(dòng)態(tài)代理 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, // 使用 Plugin 包裝代理實(shí)現(xiàn) new Plugin(target, interceptor, signatureMap)); } return target; } // pageInterceptor的注解, 即定義要攔截的方法列表@Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), }) // 過濾代理的接口 private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { Set> interfaces = new HashSet<>(); while (type != null) { for (Class c : type.getInterfaces()) { // 只有設(shè)置了的接口才會(huì)被添加 if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class[interfaces.size()]); }

          這樣, interceptor 就和executor綁定了, 后續(xù)的查詢將會(huì)看到interceptor 的作用.

          4.3 interceptor的調(diào)用過程

          在executor被代理后, 會(huì)繼續(xù)執(zhí)行查詢動(dòng)作, 這時(shí)就會(huì)被interceptor攔截了.

          // org.apache.ibatis.plugin.Plugin#invoke  @Override  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    try {      Set methods = signatureMap.get(method.getDeclaringClass());      if (methods != null && methods.contains(method)) {        // 匹配的方法會(huì)被攔截, 即 query 方法        return interceptor.intercept(new Invocation(target, method, args));      }      return method.invoke(target, args);    } catch (Exception e) {      throw ExceptionUtil.unwrapThrowable(e);    }  }    // pageHelper 正式起作用的入口    // com.github.pagehelper.PageInterceptor#intercept    @Override    public Object intercept(Invocation invocation) throws Throwable {        try {            Object[] args = invocation.getArgs();            MappedStatement ms = (MappedStatement) args[0];            Object parameter = args[1];            RowBounds rowBounds = (RowBounds) args[2];            ResultHandler resultHandler = (ResultHandler) args[3];            Executor executor = (Executor) invocation.getTarget();            CacheKey cacheKey;            BoundSql boundSql;            //由于邏輯關(guān)系,只會(huì)進(jìn)入一次            if (args.length == 4) {                //4 個(gè)參數(shù)時(shí)                boundSql = ms.getBoundSql(parameter);                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);            } else {                //6 個(gè)參數(shù)時(shí)                cacheKey = (CacheKey) args[4];                boundSql = (BoundSql) args[5];            }            checkDialectExists();
          List resultList; //調(diào)用方法判斷是否需要進(jìn)行分頁,如果不需要,直接返回結(jié)果 if (!dialect.skip(ms, parameter, rowBounds)) { //判斷是否需要進(jìn)行 count 查詢 if (dialect.beforeCount(ms, parameter, rowBounds)) { //查詢總數(shù) Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql); //處理查詢總數(shù),返回 true 時(shí)繼續(xù)分頁查詢,false 時(shí)直接返回 if (!dialect.afterCount(count, parameter, rowBounds)) { //當(dāng)查詢總數(shù)為 0 時(shí),直接返回空的結(jié)果 return dialect.afterPage(new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { //rowBounds用參數(shù)值,不使用分頁插件處理時(shí),仍然支持默認(rèn)的內(nèi)存分頁 resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { if(dialect != null){ dialect.afterAll(); } } }

          以上就是 pageHelper 的大體執(zhí)行框架了:

              1. 先解析各位置參數(shù);
              2. 初始化 pageHelper 實(shí)例, 即 dialect;
              3. 調(diào)用方法判斷是否需要進(jìn)行分頁,如果不需要,直接返回結(jié)果;
              4. 判斷是否要進(jìn)行count, 如果需要?jiǎng)t實(shí)現(xiàn)一次count, ;
              5. 查詢分頁結(jié)果;
              6. 封裝帶分頁的結(jié)果返回;

          下面我們就每個(gè)細(xì)節(jié)依次看看實(shí)現(xiàn)吧.

          4.4 是否跳過分頁判定

          首先會(huì)進(jìn)行是否需要跳過分頁邏輯,如果跳過, 則直接執(zhí)行mybatis的核心邏輯繼續(xù)查詢. 而是否要跳過分頁, 則是通過直接獲取page分頁參數(shù)來決定的,沒有分頁參數(shù)設(shè)置,則跳過, 否則執(zhí)行分頁查詢. 這算是分頁的一個(gè)入口判定呢。

           /**     * 跳過 count 和 分頁查詢     *     * @param ms              MappedStatement     * @param parameterObject 方法參數(shù)     * @param rowBounds       分頁參數(shù)     * @return true 跳過,返回默認(rèn)查詢結(jié)果,false 執(zhí)行分頁查詢     */     // com.github.pagehelper.PageHelper#skip    @Override    public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {        if (ms.getId().endsWith(MSUtils.COUNT)) {            throw new RuntimeException("在系統(tǒng)中發(fā)現(xiàn)了多個(gè)分頁插件,請(qǐng)檢查系統(tǒng)配置!");        }        // 如果 page 返回null, 則不需要進(jìn)行分頁, 即是否調(diào)用  PageHelper.start(pageNo, pageSize) 方法        Page page = pageParams.getPage(parameterObject, rowBounds);        if (page == null) {            return true;        } else {            //設(shè)置默認(rèn)的 count 列            if (StringUtil.isEmpty(page.getCountColumn())) {                page.setCountColumn(pageParams.getCountColumn());            }            autoDialect.initDelegateDialect(ms);            return false;        }    }    // com.github.pagehelper.page.PageAutoDialect#initDelegateDialect    //多數(shù)據(jù)動(dòng)態(tài)獲取時(shí),每次需要初始化    public void initDelegateDialect(MappedStatement ms) {        if (delegate == null) {            if (autoDialect) {                // 比如 MySqlDialect                this.delegate = getDialect(ms);            } else {                dialectThreadLocal.set(getDialect(ms));            }        }    }
          /** * 獲取分頁參數(shù) */ // com.github.pagehelper.page.PageParams#getPage public Page getPage(Object parameterObject, RowBounds rowBounds) { Page page = PageHelper.getLocalPage(); if (page == null) { if (rowBounds != RowBounds.DEFAULT) { if (offsetAsPageNum) { page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount); } else { page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount); //offsetAsPageNum=false的時(shí)候,由于PageNum問題,不能使用reasonable,這里會(huì)強(qiáng)制為false page.setReasonable(false); } if(rowBounds instanceof PageRowBounds){ PageRowBounds pageRowBounds = (PageRowBounds)rowBounds; page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount()); } } else if(parameterObject instanceof IPage || supportMethodsArguments){ try { page = PageObjectUtil.getPageFromObject(parameterObject, false); } catch (Exception e) { return null; } } if(page == null){ return null; } PageHelper.setLocalPage(page); } //分頁合理化 if (page.getReasonable() == null) { page.setReasonable(reasonable); } //當(dāng)設(shè)置為true的時(shí)候,如果pagesize設(shè)置為0(或RowBounds的limit=0),就不執(zhí)行分頁,返回全部結(jié)果 if (page.getPageSizeZero() == null) { page.setPageSizeZero(pageSizeZero); } return page; }

          才上判定決定了后續(xù)的分頁效果,主要是利用 ThreadLocal 來保存分頁信息,從而與用戶代碼產(chǎn)生關(guān)聯(lián)。

          4.5 pageHelper 的 count 操作

          判斷是否是否需要count,? 這些判定都會(huì)以 PageHelper 作為門面類進(jìn)行接入, 而特殊地方則由具體方言實(shí)現(xiàn).

          // com.github.pagehelper.PageHelper#beforeCount    @Override    public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {        return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);    }
          // com.github.pagehelper.dialect.AbstractHelperDialect#beforeCount @Override public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) { // 獲取page參數(shù)信息, 該參數(shù)設(shè)置在 ThreadLocal 中 Page page = getLocalPage(); return !page.isOrderByOnly() && page.isCount(); } // 如果需要進(jìn)行count, 則需要自行組裝count邏輯進(jìn)行查詢. // com.github.pagehelper.PageInterceptor#count private Long count(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 在原有l(wèi)ist 查詢后添加 _COUNT 代表count查詢id String countMsId = ms.getId() + countSuffix; Long count; //先判斷是否存在手寫的 count 查詢 MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId); if (countMs != null) { count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { countMs = msCountMap.get(countMsId); //自動(dòng)創(chuàng)建 if (countMs == null) { //根據(jù)當(dāng)前的 ms 創(chuàng)建一個(gè)返回值為 Long 類型的 ms countMs = MSUtils.newCountMappedStatement(ms, countMsId); msCountMap.put(countMsId, countMs); } count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler); } return count; } // 創(chuàng)建count ms // com.github.pagehelper.util.MSUtils#newCountMappedStatement(org.apache.ibatis.mapping.MappedStatement, java.lang.String) public static MappedStatement newCountMappedStatement(MappedStatement ms, String newMsId) { // 直接基于原有 sql 構(gòu)建新的 MappedStatement MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), newMsId, ms.getSqlSource(), ms.getSqlCommandType()); builder.resource(ms.getResource()); // 注意此處并未使用到用戶設(shè)置的分頁參數(shù) builder.fetchSize(ms.getFetchSize()); builder.statementType(ms.getStatementType()); builder.keyGenerator(ms.getKeyGenerator()); 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()); } builder.timeout(ms.getTimeout()); builder.parameterMap(ms.getParameterMap()); //count查詢返回值int List resultMaps = new ArrayList(); ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, EMPTY_RESULTMAPPING).build(); resultMaps.add(resultMap); builder.resultMaps(resultMaps); builder.resultSetType(ms.getResultSetType()); builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache());
          return builder.build(); }
          /** * 執(zhí)行自動(dòng)生成的 count 查詢 */ // com.github.pagehelper.util.ExecutorUtil#executeAutoCount public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs, Object parameter, BoundSql boundSql, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { Map additionalParameters = getAdditionalParameter(boundSql); //創(chuàng)建 count 查詢的緩存 key CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql); //調(diào)用方言獲取 count sql String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey); //countKey.update(countSql); BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter); //當(dāng)使用動(dòng)態(tài) SQL 時(shí),可能會(huì)產(chǎn)生臨時(shí)的參數(shù),這些參數(shù)需要手動(dòng)設(shè)置到新的 BoundSql 中 for (String key : additionalParameters.keySet()) { countBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執(zhí)行 count 查詢 Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql); Long count = (Long) ((List) countResultList).get(0); return count; } // com.github.pagehelper.PageHelper#getCountSql @Override public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) { // 委托給各方言實(shí)現(xiàn) sql 組裝 return autoDialect.getDelegate().getCountSql(ms, boundSql, parameterObject, rowBounds, countKey); }
          // com.github.pagehelper.dialect.AbstractHelperDialect#getCountSql @Override public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) { Page page = getLocalPage(); String countColumn = page.getCountColumn(); if (StringUtil.isNotEmpty(countColumn)) { return countSqlParser.getSmartCountSql(boundSql.getSql(), countColumn); } return countSqlParser.getSmartCountSql(boundSql.getSql()); }
          /** * 獲取智能的countSql * * @param sql * @param name 列名,默認(rèn) 0 * @return */ // com.github.pagehelper.parser.CountSqlParser#getSmartCountSql(java.lang.String, java.lang.String) public String getSmartCountSql(String sql, String name) { //解析SQL Statement stmt = null; //特殊sql不需要去掉order by時(shí),使用注釋前綴 if(sql.indexOf(KEEP_ORDERBY) >= 0){ return getSimpleCountSql(sql, name); } try { stmt = CCJSqlParserUtil.parse(sql); } catch (Throwable e) { //無法解析的用一般方法返回count語句 return getSimpleCountSql(sql, name); } Select select = (Select) stmt; SelectBody selectBody = select.getSelectBody(); try { //處理body-去order by processSelectBody(selectBody); } catch (Exception e) { //當(dāng) sql 包含 group by 時(shí),不去除 order by return getSimpleCountSql(sql, name); } //處理with-去order by processWithItemsList(select.getWithItemsList()); //處理為count查詢 sqlToCount(select, name); String result = select.toString(); return result; } /** * 將sql轉(zhuǎn)換為count查詢 * * @param select */ // com.github.pagehelper.parser.CountSqlParser#sqlToCount public void sqlToCount(Select select, String name) { SelectBody selectBody = select.getSelectBody(); // 是否能簡(jiǎn)化count查詢 List COUNT_ITEM = new ArrayList(); // 如 select * from user 將會(huì)被轉(zhuǎn)化為 select count(0) from user COUNT_ITEM.add(new SelectExpressionItem(new Column("count(" + name +")"))); if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) { // 簡(jiǎn)單sql直接轉(zhuǎn)換select字段為 count(0) 即可, 而這個(gè)sql是否支持這種方式則得仔細(xì)驗(yàn)證 ((PlainSelect) selectBody).setSelectItems(COUNT_ITEM); } else { // 如果對(duì)于復(fù)雜的sql查詢, 則只能在現(xiàn)有sql外圍加一個(gè) select count(0) from (xxxxx) as table_count PlainSelect plainSelect = new PlainSelect(); SubSelect subSelect = new SubSelect(); subSelect.setSelectBody(selectBody); subSelect.setAlias(TABLE_ALIAS); // 將原sql作為臨時(shí)表放入 plainSelect 中 plainSelect.setFromItem(subSelect); plainSelect.setSelectItems(COUNT_ITEM); // 替換原有 select select.setSelectBody(plainSelect); } } /** * 是否可以用簡(jiǎn)單的count查詢方式 */ // net.sf.jsqlparser.statement.select.PlainSelect public boolean isSimpleCount(PlainSelect select) { //包含group by的時(shí)候不可以 if (select.getGroupBy() != null) { return false; } //包含distinct的時(shí)候不可以 if (select.getDistinct() != null) { return false; } for (SelectItem item : select.getSelectItems()) { //select列中包含參數(shù)的時(shí)候不可以,否則會(huì)引起參數(shù)個(gè)數(shù)錯(cuò)誤 if (item.toString().contains("?")) { return false; } //如果查詢列中包含函數(shù),也不可以,函數(shù)可能會(huì)聚合列 if (item instanceof SelectExpressionItem) { Expression expression = ((SelectExpressionItem) item).getExpression(); if (expression instanceof Function) { String name = ((Function) expression).getName(); if (name != null) { String NAME = name.toUpperCase(); if(skipFunctions.contains(NAME)){ //go on } else if(falseFunctions.contains(NAME)){ return false; } else { for (String aggregateFunction : AGGREGATE_FUNCTIONS) { if(NAME.startsWith(aggregateFunction)){ falseFunctions.add(NAME); return false; } } skipFunctions.add(NAME); } } } } } return true; }

          大體上講就是分析sql, 如果是簡(jiǎn)單查詢, 則直接將字段內(nèi)容轉(zhuǎn)換為 count(0) 即可, 這和我們普通認(rèn)為的在select外部簡(jiǎn)單包一層還不太一樣哦. 但是對(duì)于復(fù)雜查詢?cè)蹅冞€是只能使用外包一層的實(shí)現(xiàn)方式了. 當(dāng)然了,以上實(shí)現(xiàn)是針對(duì)mysql的,其他語言可能會(huì)有不一樣的實(shí)現(xiàn).

          4.6 select list 的改裝

          在執(zhí)行完count后, 分頁的功能完成了一半. 我們可以給到用戶這個(gè)計(jì)數(shù)值, 另外,我們可以根據(jù)該值得到后續(xù)分頁還有多少數(shù)據(jù), 如果沒有自然不用再查了, 如果有則組裝limit語句.

          // com.github.pagehelper.dialect.AbstractHelperDialect#afterCount    @Override    public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {        Page page = getLocalPage();        page.setTotal(count);        if (rowBounds instanceof PageRowBounds) {            ((PageRowBounds) rowBounds).setTotal(count);        }        //pageSize < 0 的時(shí)候,不執(zhí)行分頁查詢        //pageSize = 0 的時(shí)候,還需要執(zhí)行后續(xù)查詢,但是不會(huì)分頁        if (page.getPageSize() < 0) {            return false;        }        // 還沒到最后一頁, 則需要進(jìn)行分頁查詢        return count > ((page.getPageNum() - 1) * page.getPageSize());    }
          /** * 分頁查詢 */ public static List pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException { //判斷是否需要進(jìn)行分頁查詢 if (dialect.beforePage(ms, parameter, rowBounds)) { //生成分頁的緩存 key CacheKey pageKey = cacheKey; //處理參數(shù)對(duì)象, 將會(huì)加入 pageStart, pageSize 等參數(shù) parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); //調(diào)用方言獲取分頁 sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
          Map additionalParameters = getAdditionalParameter(boundSql); //設(shè)置動(dòng)態(tài)參數(shù) for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } //執(zhí)行分頁查詢 return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不執(zhí)行分頁的情況下,也不執(zhí)行內(nèi)存分頁 return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } } // com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject @Override public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) { //處理參數(shù) Page page = getLocalPage(); //如果只是 order by 就不必處理參數(shù) if (page.isOrderByOnly()) { return parameterObject; } Map paramMap = null; if (parameterObject == null) { paramMap = new HashMap(); } else if (parameterObject instanceof Map) { //解決不可變Map的情況 paramMap = new HashMap(); paramMap.putAll((Map) parameterObject); } else { paramMap = new HashMap(); //動(dòng)態(tài)sql時(shí)的判斷條件不會(huì)出現(xiàn)在ParameterMapping中,但是必須有,所以這里需要收集所有的getter屬性 //TypeHandlerRegistry可以直接處理的會(huì)作為一個(gè)直接使用的對(duì)象進(jìn)行處理 boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); MetaObject metaObject = MetaObjectUtil.forObject(parameterObject); //需要針對(duì)注解形式的MyProviderSqlSource保存原值 if (!hasTypeHandler) { for (String name : metaObject.getGetterNames()) { paramMap.put(name, metaObject.getValue(name)); } } //下面這段方法,主要解決一個(gè)常見類型的參數(shù)時(shí)的問題 if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) { for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { String name = parameterMapping.getProperty(); if (!name.equals(PAGEPARAMETER_FIRST) && !name.equals(PAGEPARAMETER_SECOND) && paramMap.get(name) == null) { if (hasTypeHandler || parameterMapping.getJavaType().equals(parameterObject.getClass())) { paramMap.put(name, parameterObject); break; } } } } } return processPageParameter(ms, paramMap, page, boundSql, pageKey); }
          // 加入 page 參數(shù) // com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter @Override public Object processPageParameter(MappedStatement ms, Map paramMap, Page page, BoundSql boundSql, CacheKey pageKey) { // First_PageHelper, Second_PageHelper paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow()); paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize()); //處理pageKey pageKey.update(page.getStartRow()); pageKey.update(page.getPageSize()); //處理參數(shù)配置 if (boundSql.getParameterMappings() != null) { List newParameterMappings = new ArrayList(boundSql.getParameterMappings()); if (page.getStartRow() == 0) { newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build()); } else { newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, Integer.class).build()); newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build()); } MetaObject metaObject = MetaObjectUtil.forObject(boundSql); metaObject.setValue("parameterMappings", newParameterMappings); } return paramMap; } // 組裝分頁sql // com.github.pagehelper.dialect.AbstractHelperDialect#getPageSql @Override public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) { String sql = boundSql.getSql(); Page page = getLocalPage(); //支持 order by String orderBy = page.getOrderBy(); if (StringUtil.isNotEmpty(orderBy)) { pageKey.update(orderBy); sql = OrderByParser.converToOrderBySql(sql, orderBy); } if (page.isOrderByOnly()) { return sql; } return getPageSql(sql, page, pageKey); } // com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql @Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); // 分頁sql拼接, limit xxx if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT ? "); } else { sqlBuilder.append(" LIMIT ?, ? "); } return sqlBuilder.toString(); }

          經(jīng)過上面的sql重組之后,就可以得到具體分頁的list數(shù)據(jù)了, 返回的也是list數(shù)據(jù). 那么, 用戶如何獲取其他的分頁信息呢? 比如count值去了哪里? 實(shí)際上, 在list 返回之后, 還有一個(gè) afterPage 的動(dòng)作要做, 而它的作用就是封裝list 為帶page信息的list.

           // com.github.pagehelper.PageHelper#afterPage    @Override    public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {        //這個(gè)方法即使不分頁也會(huì)被執(zhí)行,所以要判斷 null        AbstractHelperDialect delegate = autoDialect.getDelegate();        if (delegate != null) {            return delegate.afterPage(pageList, parameterObject, rowBounds);        }        return pageList;    }
          // com.github.pagehelper.dialect.AbstractHelperDialect#afterPage @Override public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) { // 取出本線程的page變量, 放入list Page page = getLocalPage(); if (page == null) { return pageList; } page.addAll(pageList); // count 值臨時(shí)變換, 用于應(yīng)對(duì)沒有進(jìn)行count的場(chǎng)景, 使外部表現(xiàn)一致 if (!page.isCount()) { page.setTotal(-1); } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) { page.setTotal(pageList.size()); } else if(page.isOrderByOnly()){ page.setTotal(pageList.size()); } return page; }

          至此, 一個(gè)完整的分頁功能就完成了. 核心邏輯最開始也已看到, 就是判斷是否需要分頁, 是否需要count, 然后添加分頁sql取數(shù)的這么個(gè)過程. 其本身并無太多銀彈, 但卻是能讓我們節(jié)省不少時(shí)間. 另外就是, 在應(yīng)對(duì)數(shù)據(jù)庫可能發(fā)生切換的場(chǎng)景, 我們也可以無需更改此部分代碼, 從而減輕了歷史負(fù)擔(dān). 用用又何樂而不為呢?

          最后, 我們?cè)賮砜聪耾racle的核心分頁的時(shí)候, 以理解pagehelper 的良苦用心.

          5. oracle sql 變換

          前面我們以mysql為樣例, 看了pagehelper的轉(zhuǎn)換過程, 其核心自然是 對(duì)count和select sql 的變換. 下面我們看看oracle如何變換吧!

          // com.github.pagehelper.dialect.helper.OracleDialectpublic class OracleDialect extends AbstractHelperDialect {
          @Override public Object processPageParameter(MappedStatement ms, Map paramMap, Page page, BoundSql boundSql, CacheKey pageKey) { paramMap.put(PAGEPARAMETER_FIRST, page.getEndRow()); paramMap.put(PAGEPARAMETER_SECOND, page.getStartRow()); //處理pageKey pageKey.update(page.getEndRow()); pageKey.update(page.getStartRow()); //處理參數(shù)配置 handleParameter(boundSql, ms); return paramMap; } // 獲取帶分頁的sql @Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120); // 很明顯, oracle 和 mysql 的分頁實(shí)現(xiàn)是不一樣的, oracle 使用 row_id 實(shí)現(xiàn), 而 mysql 使用 limit 實(shí)現(xiàn) sqlBuilder.append("SELECT * FROM ( "); sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( "); sqlBuilder.append(sql); sqlBuilder.append(" ) TMP_PAGE)"); sqlBuilder.append(" WHERE ROW_ID <= ? AND ROW_ID > ?"); return sqlBuilder.toString(); }
          }

          從OracleDialect的實(shí)現(xiàn)中,我們看到它與mysql的差異僅在參數(shù)設(shè)置和獲取分頁sql時(shí)的差別, count 操作都是一樣的. 雖然是這樣, 但假設(shè)我們沒有使用分頁插件, 那么你會(huì)發(fā)現(xiàn), 各個(gè)同學(xué)實(shí)現(xiàn)的count和分頁查詢相差甚大, 這必將給以后的改造帶來許多麻煩, 這就沒必要了.

          pagehelper 支持的幾個(gè)方言如下:

          ?

          它們與oracle的實(shí)現(xiàn)方式都差不多,也就是說 count 都一樣,只是分頁的sql不一樣而已。

          遺留個(gè)思考題:pagehelper通過ThreadLocal來共享分頁信息,那么它是何時(shí)進(jìn)行清除的呢?如果不清理那不就亂套了嗎?思考完成后點(diǎn)擊以下查看答案!

           // 實(shí)際上在每次運(yùn)行完成pageInterceptor之后,都會(huì)在finnaly中進(jìn)行一次清理工作        try {            // do page things        } finally {            // afterAll 即為清理任務(wù)            if(dialect != null){                dialect.afterAll();            }        }    // com.github.pagehelper.PageHelper#afterAll    @Override    public void afterAll() {        //這個(gè)方法即使不分頁也會(huì)被執(zhí)行,所以要判斷 null        AbstractHelperDialect delegate = autoDialect.getDelegate();        if (delegate != null) {            // 默認(rèn)為空            delegate.afterAll();            // delegate 移除,這里也是使用 ThreadLocal 實(shí)現(xiàn),直接remove即可            autoDialect.clearDelegate();        }        // 清理 page對(duì)象,下次不再有該設(shè)置,也就是說 page 設(shè)置是一次性的        clearPage();    }    // com.github.pagehelper.page.PageMethod#clearPage    public static void clearPage() {        LOCAL_PAGE.remove();    }    // 下次再進(jìn)行分頁時(shí),重新調(diào)用  PageHelper.startPage(x, x, x); 即可


          往期精彩推薦



          騰訊、阿里、滴滴后臺(tái)面試題匯總總結(jié) — (含答案)

          面試:史上最全多線程面試題 !

          最新阿里內(nèi)推Java后端面試題

          JVM難學(xué)?那是因?yàn)槟銢]認(rèn)真看完這篇文章


          END


          關(guān)注作者微信公眾號(hào) —《JAVA爛豬皮》


          了解更多java后端架構(gòu)知識(shí)以及最新面試寶典


          你點(diǎn)的每個(gè)好看,我都認(rèn)真當(dāng)成了


          看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動(dòng)力

          作者:等你歸去來

          出處:https://www.cnblogs.com/yougewe/p/13513892.html

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    日韩高清一级免费 | 十八女人毛片 | 婷婷五月天大香蕉 | 色香蕉视频 | 无码人妻aV一区二区三区色欲 |