在《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 }