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