• 编码技巧——Mybatis分页插件PageHelper使用


    本篇介绍Mybatis分页插件PageHelper的使用,本质上是Mybatis拦截器起作用,在执行目标SQL之前添加count语句及拼接limit到目标SQL后面,这里不再赘述,Mybatis拦截器相关知识可参考Mybatis——拦截器Interceptor,本篇仅简单介绍如何让一个使用Mybatis+springboot的项目快速接入PageHelper

    背景:接到需求,需要对一个老工程新写几个分页查询及导出的接口;查看了这个工程的代码,发现历史的分页接口竟然是手动拼出来的,非常之丑陋,准备引入Mybatis自带的分页插件,顺带记录下遇到的问题和最终引入PageHelper的步骤;

    遇到问题1:根据经验,只需要引入一个PageHelper的二方包,在springboot下,会自动注册Mybatis Interceptor并生效;发现SQL可以正常执行,但是分页参数都没有填充;所以问题应该是PageHelper并没有被正确的加入Mybatis拦截器列表,这一步可以通过打印原始的SQL来验证,如果工程没有开启SQL日志,可参考《编码技巧——Mybatis输出SQL日志》

    解决方案:继续引入pagehelper-spring-boot-starter并在配置类加载PageHelper的Bean;

    遇到问题2:按照上述方案成功的引入了pagehelper,并且分页参数都被正常的填充;但是当把分页插件原生的com.github.pagehelper.Page作为返参返回时,发现缺失了分页参数如total、pageNum、pageSize等;发现Page其实仅继承了java.util.ArrayList,因此序列化的时候会当做一个List处理,分页参数都缺失了;

    解决方案:自己封装分页返参实体类;

    总结下正确引入Mybatis分页插件PageHelper的姿势

    (1)引入依赖

    以下依赖建议放到dal模块:

    1. <dependency>
    2. <groupId>com.github.pagehelpergroupId>
    3. <artifactId>pagehelperartifactId>
    4. <version>5.2.0version>
    5. dependency>

    以下依赖建议放到启动/配置boot模块

    1. <dependency>
    2. <groupId>com.github.pagehelpergroupId>
    3. <artifactId>pagehelper-spring-boot-starterartifactId>
    4. <version>1.2.12version>
    5. dependency>

    (2)配置Bean

    1. @Configuration
    2. public class PageHelperConfig {
    3. @Bean
    4. public PageHelper pageHelper() {
    5. PageHelper pageHelper = new PageHelper();
    6. Properties properties = new Properties();
    7. properties.setProperty("offsetAsPageNum", "true");
    8. properties.setProperty("rowBoundsWithCount", "true");
    9. properties.setProperty("reasonable", "true");
    10. properties.setProperty("dialect", "mysql");
    11. pageHelper.setProperties(properties);
    12. return pageHelper;
    13. }
    14. }

    (3)封装返参

    1. /**
    2. * @author Akira
    3. * @description 默认的分页查询的封装类
    4. */
    5. @Data
    6. public class DefaultPageDTO {
    7. /**
    8. * 页码
    9. */
    10. private Integer pageNum;
    11. /**
    12. * 页面大小,默认size为10
    13. */
    14. private Integer pageSize;
    15. /**
    16. * 总数
    17. */
    18. private Long total;
    19. /**
    20. * 总页数
    21. */
    22. private Integer pages;
    23. /**
    24. * 查询记录列表
    25. */
    26. private List records;
    27. /**
    28. * 将com.github.pagehelper.Page封装成能打印出分页参数的DefaultPageDTO
    29. *
    30. * @param page
    31. * @param
    32. * @return
    33. */
    34. public static DefaultPageDTO init(Page page, Function converter) {
    35. DefaultPageDTO defaultPageDTO = new DefaultPageDTO<>();
    36. defaultPageDTO.setPageNum(page.getPageNum());
    37. defaultPageDTO.setPages(page.getPages());
    38. defaultPageDTO.setTotal(page.getTotal());
    39. defaultPageDTO.setPageSize(page.getPageSize());
    40. if (page.getTotal() > 0) {
    41. defaultPageDTO.setRecords(page.getResult().stream().map(converter).collect(Collectors.toList()));
    42. }
    43. return defaultPageDTO;
    44. }
    45. }

    补充1:条件查询的一般写法

    (1)定义好DAO接口、xml、实体,如使用Mybatis代码生成器生成;

    (2)定义controller层的查询实体,以及数据库层的查询实体,二者可以使用Bean转换工具MapStruct做转换;数据库层的查询实体可以继承原实体,并附带上一些额外的查询条件,如批量查询、时间区间、排序规则等;示例如下:

    1. @Data
    2. public class UserAuditFlowQuery extends UserAuditFlowDO {
    3. /**
    4. * 时间区间
    5. */
    6. private Date submitTimeLeft;
    7. private Date submitTimeRight;
    8. /**
    9. * 批量查询
    10. */
    11. private List flowIdList;
    12. /**
    13. * 排序规则
    14. */
    15. private Boolean orderByFlowId;
    16. }

    (3)在xml中使用标签拼接SQL语句

    1. /**
    2. * 分页查询
    3. *
    4. * @param pageQuery
    5. * @return
    6. */
    7. public DefaultPageDTO pageQueryMyReading(AuditFlowMyReadingQuery pageQuery) {
    8. PageHelper.startPage(pageQuery.getPage(), pageQuery.getPageSize());
    9. final UserAuditFlowQuery query = beanConvertMapper.convert2UserAuditFlowQuery(pageQuery);
    10. final Page page = userAuditFlowDAO.pageQueryByCondition(query);
    11. final DefaultPageDTO defaultPageDTO = DefaultPageDTO.init(page, domain -> beanConvertMapper.convert2UserAuditFlowDTO(domain));
    12. return defaultPageDTO;
    13. }
        Page pageQueryByCondition(@Param("condition") UserAuditFlowQuery userAuditFlowQuery);
    
    1. <select id="pageQueryByCondition" resultMap="BaseResultMap">
    2. select
    3. <include refid="Base_Column_List"/>
    4. from user_audit_flow
    5. <where>
    6. <if test="condition.title != null and condition.title != ''">
    7. and `title` like "%"#{condition.title,jdbcType=VARCHAR}"%"
    8. if>
    9. <if test="condition.workNumber != null and condition.workNumber != ''">
    10. and `work_number` = #{condition.workNumber,jdbcType=VARCHAR}
    11. if>
    12. <if test="condition.roleCode != null and condition.roleCode != ''">
    13. and `role_code` = #{condition.roleCode,jdbcType=VARCHAR}
    14. if>
    15. <if test="condition.flowId != null">
    16. and `flow_id` = #{condition.flowId,jdbcType=BIGINT}
    17. if>
    18. <if test="condition.secondSubModuleCode != null and condition.secondSubModuleCode != ''">
    19. and `second_sub_module_code` = #{condition.secondSubModuleCode,jdbcType=VARCHAR}
    20. if>
    21. <if test="condition.auditStatus != null">
    22. and `audit_status` = #{condition.auditStatus,jdbcType=INTEGER}
    23. if>
    24. <if test="condition.submitor != null and condition.submitor != ''">
    25. and `submitor` = #{condition.submitor,jdbcType=VARCHAR}
    26. if>
    27. <if test="condition.submitTimeLeft != null and condition.submitTimeRight != null">
    28. and `create_time` between #{condition.submitTimeLeft,jdbcType=TIMESTAMP} and
    29. #{condition.submitTimeRight,jdbcType=TIMESTAMP}
    30. if>
    31. where>
    32. <if test="condition.orderByFlowId != null">
    33. <if test="condition.orderByFlowId == true">
    34. order by flow_id
    35. if>
    36. <if test="condition.orderByFlowId == false">
    37. order by flow_id desc
    38. if>
    39. if>
    40. select>

    补充2:遍历表数据

    使用limit语句遍历表,当表数据量很大时,limit扫描的行数会越来越多,导致查询变慢;比如说limit 10000,10,MYSQL会去查询10010条数据,然后返回最后10条数据;当表数据量很大时,越往后需要查询的数据量越来越大,导致时间越来越长;

    常用的解决方案有:

    1. 记录主键id,例如顺序查询遍历表数据的场景,从上次查询结果中记录最后一位主键id,在查询语句中使用主键范围条件,减小回表次数;如:

    select * from tableA where id > 10000 order by id limit 10;

    2. 嵌套子查询,先查主键id,再根据选好的pageSize数量的主键id回表查,减小扫描的数据量;在有二级索引的情况下,查主键id会遍历空间大小更小二级索引树而非聚簇索引树,扫描的速度更快;注意,这种语句可能较低的MySQL版本不支持,可以在代码中分别执行2句SQL或改为使用join语句;

    select * from tableA where id in (select id from tableA order by id LIMIT 10000,10);

    改为:

    1. select id from audit_flow_main order by id LIMIT 10000,10;
    2. select * from audit_flow_main where id IN (...);

    以上语句也可以通过使用join语句改写,原理是一样的:

    SELECT * FROM tableA a JOIN (SELECT b.id FROM tableA b order by id LIMIT 5000,10) AS c on a.id = c.id;

    注意:如果要按照主键id遍历数据,则需要在limit前加上order by id,否则查出来的数据与扫描主键索引查出来的数据不一致!原因很简单,二级索引有序,二级索引的叶子节点存放的主键id无序;

     3. 使用between语句,如果仅遍历表数据,对每页数据大小没有严格要求时,如执行一些扫描表的定时任务时,推荐使用对主键做between,即手动算出每次查询的主键id区间,这样扫描的行数是最小的,效率也很高;一个定时任务的代码示例如下:

    1. public ExecutionResult execute(ExecutionContext executionContext) {
    2. long maxId = auditMainFlowMapper.selectMaxId();
    3. // 执行时就确定好id上限,仅刷新当前时刻的所有数据
    4. long idRight = maxId;
    5. long idLeft = Math.max(1, idRight - pageSize + 1);
    6. log.warn("任务开始");
    7. while (true) {
    8. if (idRight <= 1) {
    9. log.warn("任务跳出");
    10. break;
    11. }
    12. final QueryCondition queryCondition = new QueryCondition();
    13. queryCondition.setIdLeft(idLeft);
    14. queryCondition.setIdRight(idRight);
    15. final List pageData = mDataMapper.selectByCondition(queryCondition);
    16. if (CollectionUtils.isNotEmpty(pageData)) {
    17. try {
    18. doTaskForCurrentPage(pageData);
    19. } catch (Exception e) {
    20. log.warn("当前批次执行失败, e);
    21. }
    22. }
    23. // 翻页
    24. idRight = idRight - pageSize;
    25. idLeft = Math.max(1, idRight - pageSize + 1);
    26. }
    27. log.warn("任务结束");
    28. return ExecutionResult.SUCCESS;
    29. }
    1. <select id="selectByCondition" resultMap="BaseResultMap">
    2. select
    3. <include refid="Base_Column_List"/>
    4. from my_data
    5. <where>
    6. <if test="condition.idLeft != null and condition.idRight != null">
    7. and `id` between #{condition.idLeft,jdbcType=BIGINT} and #{condition.idRight,jdbcType=BIGINT}
    8. if>
    9. where>
    10. select>

  • 相关阅读:
    sql常用语法记录
    http 跨域资源共享详解
    7、Java 对象在 JVM 中的内存布局(详细说明)
    2022牛客多校(四)
    力扣(LeetCode)123. 买卖股票的最佳时机 III(C++)
    22-09-02 西安 JVM(01)类加载器、stack栈、堆体系、堆参数调优、GC垃圾判定、垃圾回收算法
    陪诊系统|陪诊软件革新陪诊体验解决病患难题
    OpenHarmony应用开发【01】
    07.OSPF的七种LSA类型
    nacos的学习
  • 原文地址:https://blog.csdn.net/minghao0508/article/details/128080443