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