pagehelper一對多分頁查詢問題優化


在《mybatis包含一對多的分頁查詢問題詳解》這篇文章里介紹了mybatis利用pagehelper分頁查詢會出現分頁不准確的問題,同時文章中也寫了利用mybatis子查詢的解決方案,之前面試被問到這個問題,我也按這篇文章里的答案做了回答,但好像不是面試官要的答案,他說子查詢的這種方案效率太低,還有更好的解決方式,但當時確實是想不到其他的方案。后面自己也查了一些資料,確實有其他更優的解決方式,所以新寫一篇文章記錄下來。

  延用上一篇文章的例子:

  查詢列表頁是展示各種手機信息,有一列是要展示這種手機所有的內存,比如華為P30有218G,256G,512G,具體的表結構和mybatis文件如下,數據庫為mysql數據庫。

     create table TF_L_PHONE(

ID VARCHAR(32) primary key comment 'ID',
PHONE_BRAND VARCHAR(60) comment '手機品牌',
PHONE_NAME VARCHAR(60) comment '手機名稱',
PHONE_CODE VARCHAR(260) comment '手機編碼',
PHONE_DESC VARCHAR(2240) comment '手機描述',
MARKET_TIME TIMESTAMP comment '手機上市時間'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC comment '手機主表';

create table TF_L_PHONE_RAM(
ID VARCHAR(32) primary key comment 'ID',
PHONE_ID VARCHAR(32) comment '手機id',
PHONE_RAM int(4) comment '手機內存',
PHONE_FEE int(8) comment '手機價格 單位是分'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC comment '手機內存類型表'; 

 

 

  一:我們先看下未使用pageHelper境況下查詢語句:

    1:會引起分頁不准確的語句:

      select P.*,PR.id PR_ID,PR.PHONE_RAM,PR.PHONE_FEE

      from TF_L_PHONE P left join TF_L_PHONE_RAM PR on PR.PHONE_ID = P.id

      where P.PHONE_BRAND = #{phoneBrand}  limit #{startNum},#{endNum}

 這個語句分頁查詢語句就會產生如下圖所示分頁不准確的問題問題,每頁應該是5條,但有的頁是一條數據,有的頁是兩條數據,因為先對left join查詢數據做了分頁(不是對主數據分頁),查出數據后再應該一對多實體,同一個主表id的數據就合並成了一條數據導致展示的數據不是5條。

         

   2:對主表數據分頁的方式

    select PH.*,PR.id PR_ID,PR.PHONE_RAM,PR.PHONE_FEE from

    (select P.* from TF_L_PHONE P where P.PHONE_BRAND = #{phoneBrand} limit #{startNum},#{endNum} )PH

    left join TF_L_PHONE_RAM PR on PR.PHONE_ID = P.id

    因為分頁數據是對主表進行分頁,所以就不會產生上面的分頁問題;

 

   二:使用Mybatis_PageHelper處理方案

    Mybatis_PageHelper的官方文檔地址:https://gitee.com/free/Mybatis_PageHelper/blob/master/README_zh.md

     

 

       查看官方文檔可以知道,mysql的分頁處理類是MysqlDialect,那我們看下這個類分頁是怎么實現的?

    MysqlDialect里面有兩個方法 getPageSql 和 processPageParameter

    getPageSql 方法是為了在sql最后加上LIMIT語句

    

     processPageParameter方法是為了添加分頁參數到參數Map里。

    

    如果我們mapper里sql語句是:

    select P.*,PR.id PR_ID,PR.PHONE_RAM,PR.PHONE_FEE

    from TF_L_PHONE P left join TF_L_PHONE_RAM PR on PR.PHONE_ID = P.id

    where P.PHONE_BRAND = #{phoneBrand} 

    使用 Page page = PageHelper.startPage(pageNum, pageSize);分頁查詢的時候分頁插件就會在sql的最后面幫我們加上limit ?,?,然后進行分頁查詢。

    了解了分頁查詢的原理,就會明白一堆多的分頁查詢如果我們直接使用page的原始分頁方法,就會在sql的最后幫我們加上limit?,?。那就會產生分頁查詢不准確的問題,那我么能不能像上面一樣對主表進行分頁來處理分頁問題。

    1:在原始的sql中添加一個主表分頁標識   /* MAPPINGLIMIT */

      select PH.*,PR.id PR_ID,PR.PHONE_RAM,PR.PHONE_FEE from

     (select P.* from TF_L_PHONE P where P.PHONE_BRAND = #{phoneBrand} 

     /* MAPPINGLIMIT */

     left join TF_L_PHONE_RAM PR on PR.PHONE_ID = P.id

     

    2:繼承MysqlDialect類重新其方法代碼如下

     核心思路就是根據正則表達式匹配 /* MAPPINGLIMIT * /,然后把limit?,?語句和參數插入到合適的位置,然后生成對主表分頁的語句,重寫MysqlDialect生成的sql如下,這樣查詢就不用再使用子查詢,sql查詢效率也會有所提高。

     select PH.*,PR.id PR_ID,PR.PHONE_RAM,PR.PHONE_FEE from

     (select P.* from TF_L_PHONE P where P.PHONE_BRAND = #{phoneBrand} 

      LIMIT ?,? )

     left join TF_L_PHONE_RAM PR on PR.PHONE_ID = P.id

      
 1 @Slf4j
 2 public class PageMySqlDialectPlus extends MySqlDialect {
 3 
 4     //正則表達式
 5     private static final String pattern = "([\\s|\\S]*?)/\\*\\s*MAPPINGLIMIT\\s*\\*/\\s*([\\s|\\S]*)";
 6     private static final Pattern PATTERN = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
 7 
 8     /**
 9      * 把limit語句放到 MAPPINGLIMIT標記所在的位置,也就是主表的位置,對主表進行分頁
10      * @return 加limit后的sql
11      */
12     @Override
13     public String getPageSql(String sql, Page page, CacheKey pageKey) {
14         //如果不匹配正則,走原始的sql
15         if (!Pattern.matches(pattern, sql)) {
16             return super.getPageSql(sql, page, pageKey);
17         }
18 
19         String beforeLimitSql = "";
20         String afterLimitsql = "";
21         Matcher m = PATTERN.matcher(sql);
22         if (m.find()) {
23             //MAPPINGLIMIT標記前的sql語句
24             beforeLimitSql = m.group(1);
25             //MAPPINGLIMIT標記后的sql語句
26             afterLimitsql = m.group(2);
27         }
28 
29         String limitSql = "";
30         if (page.getStartRow() == 0) {
31             limitSql = " LIMIT ? ";
32         } else {
33             limitSql = " LIMIT ?, ? ";
34         }
35         String sqlString = beforeLimitSql + " " + limitSql + " " + afterLimitsql;
36 
37         return sqlString;
38     }
39 
40     /**
41      * 把分頁參數放到參數列表里
42      * @return
43      */
44     @Override
45     public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
46         //如果不匹配正則,走原始的sql設置
47         if (!Pattern.matches(pattern, boundSql.getSql())) {
48             return super.processPageParameter(ms, paramMap, page, boundSql, pageKey);
49         }
50         //設置參數
51         paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
52         paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
53         pageKey.update(page.getStartRow());
54         pageKey.update(page.getPageSize());
55 
56         //設置參數 因為limit放到中間位置,所以要計算出來分頁數據的放置位置
57         Matcher m = PATTERN.matcher(boundSql.getSql());
58         String beforeLimitSql = null;
59         int limitIndex;
60         if (m.find()) {
61             //MAPPINGLIMIT標記前的sql語句
62             beforeLimitSql = m.group(1);
63         }
64         //計算sql里有幾個參數,按數據位置添加page
65         limitIndex = StringUtils.countMatches(beforeLimitSql, "?");
66         if (boundSql.getParameterMappings() != null) {
67             List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
68             if (page.getStartRow() == 0) {
69                 newParameterMappings.add(limitIndex, new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
70             } else {
71                 newParameterMappings.add(limitIndex + 1, new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
72                 newParameterMappings.add(limitIndex, new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, Integer.class).build());
73             }
74             MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
75             metaObject.setValue("parameterMappings", newParameterMappings);
76         }
77         return paramMap;
78     }
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM