分页-PageHelper原理以及实时分页-键集分页

一.PageHelper原理

1.使用

PageHelper 是国内非常优秀的一款开源 mybatis 分页插件,它支持常用的主流数据库,例如 Oracle、Mysql、MariaDB、SQLite、Hsqldb 等。

PageHelper 的安装很简单,只需要在 pom.xml 中加入以下依赖即可:

<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.2.0</version></dependency>

PageHelper 的使用也非常简单,只需要在查询之前调 PageHelper.startPage() 方法即可开始分页。例如:

// 开始分页PageHelper.startPage(pageNum, pageSize);// 查询List<User> userList = userService.getUserList();// 封装分页对象PageInfo<User> pageInfo = new PageInfo<>(userList);

其中,pageNum 表示要查询的页码,pageSize 表示每页的记录数。调用 startPage 方法之后,PageHelper 会自动将下一次查询作为分页查询,并且会在查询之后返回一个 Page 对象,然后可以将这个对象转换为 PageInfo 对象,从而获得分页相关的信息。

这里其实存在两个问题:

  1. 为什么查询之后会返回 Page 对象,而不是 List 对象?
  2. 为什么不直接将 list 返回,而是需要封装一次再返回PageInfo 对象?

这里我们稍后再回答,先继续说明一些 PageHelper 的一些使用技巧。

Page page = PageHelper.startPage(pageNum, pageSize, true); - true 表示需要统计总数,这样会多进行一次请求 select count(0),不传默认为 true。

1)统计总数(将SQL语句变为 select count(0) from xxx,只对简单SQL语句其效果,复杂SQL语句需要自己写)

    Page<?> page = PageHelper.startPage(1,-1);long count = page.getTotal();

2)使用PageHelper查全部(不分页)

    PageHelper.startPage(1,0);List<?> allList = queryForList( xxx.class , "queryAll" , param);

2.PageHelper的底层原理

首先调用 PageHelper 的 startPage 方法开启分页,方法中会将分页参数存到一个变量 ThreadLocal<Page> LOCAL_PAGE中;

然后调用 mapper 进行查询,这里实际上会被 PageInterceptor 类拦截,执行其重写的 interceptor 方法,该方法中主要做了以下两件事:

  • 获取到 MappedStatement,拿到业务写好的 sql,将 sql 改造成 select count(0) 并执行查询,并将执行结果存到 LOCAL_PAGE 里的Page 中的 total 属性,表示总条数
  • 获取到 xml 中的 sql 语句,并 append 一些分页 sql 段,然后执行,将执行结果存到 LOCAL_PAGE 里的 Page 中的 list 属性,这里的Page 类实际是 ArrayList 的子类。

可以看出,结果是封装到了 Page 中,最后交由 PageInfo,从中可以获取到总条数、总页数等参数。

1.分页参数储存

首先看PageHelper.startPage的源码:

 public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {Page<E> page = new Page(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}setLocalPage(page);return page;}

其实主要就是把分页参数给到 Page ,然后将实例 Page 存储到 ThreadLocal 中。

public abstract class PageMethod {protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();public PageMethod() {}protected static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}}

继续看执行SQL是怎么做的

2.拦截器改造 SQL

(1)统计总数

PageHelper 是通过拦截器底层执行 sql,对应的拦截器是 PageInterceptor,首先来看看这个类头部的定义,可以看出拦截了 Executor 的 query方法,毕竟 Mybatis 底层查询实际是借助 SqlSeesion 调用 Executor#query。

​
@Intercepts({@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})public class PageInterceptor implements Interceptor { private static final Log log = LogFactory.getLog(PageInterceptor.class); private static boolean debug = false; protected Cache<String, MappedStatement> msCountMap = null; protected CountMsIdGen countMsIdGen; private volatile Dialect dialect; private String countSuffix; private String default_dialect_class;
 

然后重点看下 intercept 方法,方法中传入的 Invocation 是JDK进行动态代理的时候,Plugin 类将反射信息封装到 Invocation 里,然后传给 intercept。

看下在哪里进行了总条数查询,进入 count 方法内看下:

继续追踪代码执行过程,发现进入 executeAutoCount 方法内,这个方法内有个变量为 countSql,其内容正是"select count(0)...",说明 PageHelper 在此处进行了总条数查询。

(2)分页查询

再看下 intercept 方法中如何进行如何分页查询:

在 pageQuery 方法中进行实际查询操作:

方法中的 pageSql 即为分页查询语句,看下 getPageSql 是如何实现的:

getPageSql 这个方法会根据不同的数据库,对 sql 进行不同的改造,这里关注下 MySql 是如何改造的:

很明显到这里就能看出 PageHelper 在针对分页查询时对每一个查询 sql 末尾都增加了 limit 子句,最大的一个问题得到了解决,代码后续就是拼接后Sql 的执行返回过程。值得注意的是,在 intercept 方法末尾的 finally 中调用 afferAll 方法对 ThreadLocal 进行 remove。

3.PageInfo

实际代码中进行分页查询得到 list之后,还要将其封装进 PageInfo 类中,才能获取到分页信息。我们关注下 PageInfo 中的构造器:

在这段代码中,将list强转为Page,再看下Page类中的设计,Page 类实际上是 ArrayList 的子类,且 Page 类中包含了分页的具体信息,而分页查询返回的 list 实际类型就是 Page,所以将其封装为 PageInfo 再返回也是合理且正确的。

2.安全性问题

PageHelper 的 startPage 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。 只要保证在 startPage 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。但是例如下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);List<Country> list;if (param1 != null) {list = countryMapper.selectIf(param1);} else {list = new ArrayList<Country>();}

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

上面这个代码,应该写成下面这个样子:

List<Country> list;if (param1 != null) {PageHelper.startPage(1, 10);list = countryMapper.selectIf(param1);} else {list = new ArrayList<Country>();}

这种写法就能保证安全。当然也可以手动清理 ThreadLocal 存储的分页参数,如下代码所示,但是这种写法不好看,且没有必要,推荐上面的写法。

List<Country> list;if (param1 != null) {PageHelper.startPage(1, 10);try {list = countryMapper.selectAll();} finally {PageHelper.clearPage();}} else {list = new ArrayList<Country>();}

 二.键集分页

Spring Boot中实现“键集分页”(Keyset Pagination)算法,主要是利用数据库的排序功能,通过记录上一次查询结果的最后一条记录的排序键(通常是主键或者唯一键),在后续的查询中利用这个排序键来获取下一页的数据。这样可以避免分页查询中的“跳页”问题,尤其是在有新数据插入时。

以下是一个基于Spring Data JPA实现键集分页的示例:

定义实体类: 假设我们有一个Post实体,它有一个主键id和一个用于排序的字段createdDate

@Entity
public class Post {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String title;@Temporal(TemporalType.TIMESTAMP)private Date createdDate;// 省略getter和setter
}

 

定义存储库接口: 创建一个继承JpaRepository的存储库接口,并添加一个支持键集分页的方法。

public interface PostRepository extends JpaRepository<Post, Long> {@Query("SELECT p FROM Post p WHERE p.createdDate > :lastCreatedDate ORDER BY p.createdDate ASC")List<Post> findPostsAfterDate(@Param("lastCreatedDate") Date lastCreatedDate, Pageable pageable);
}

 

实现服务层: 在服务层中,我们需要一个方法来处理键集分页逻辑。

@Service
public class PostService {@Autowiredprivate PostRepository postRepository;public Page<Post> getKeysetPage(Date lastCreatedDate, Pageable pageable) {// 获取分页数据List<Post> posts = postRepository.findPostsAfterDate(lastCreatedDate, pageable);// 获取总记录数,这里需要根据实际情况从数据库中查询long total = postRepository.countByCreatedDateGreaterThan(lastCreatedDate);// 创建Page对象return new PageImpl<>(posts, pageable, total);}
}

 

实现控制器: 在控制器中,我们需要处理分页请求,并提供必要的参数来调用服务层的getKeysetPage方法。

@RestController
@RequestMapping("/posts")
public class PostController {@Autowiredprivate PostService postService;@GetMappingpublic Page<Post> getPosts(@RequestParam(value = "lastCreatedDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date lastCreatedDate,@RequestParam(value = "page", defaultValue = "0") int page,@RequestParam(value = "size", defaultValue = "10") int size) {Pageable pageable = PageRequest.of(page, size);return postService.getKeysetPage(lastCreatedDate, pageable);}
}

在这个例子中,我们使用createdDate字段作为排序键。客户端在请求分页数据时,需要提供上一次查询结果的最后一条记录的createdDate。服务器端使用这个日期来查询下一页的数据。

需要注意的是,键集分页算法适用于排序字段具有唯一性的情况,例如时间戳或者自增的主键。如果排序字段不具有唯一性,可能需要结合其他字段来确保分页的准确性。此外,这种算法可能不适用于随机访问或者跳转到特定页码的操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/774130.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络类型及数据链路层协议

目录 一、网络的分类 二、数据链路层协议 1、MA网络以太网协议 2、P2P网络 3、HDLC ---高级数据链路控制协议 HDLC地址借用 三、PPP协议 1、PPP协议的优点 2、PPP数据帧封装结构 3、PPP会话的搭建 4、LCP建立——链路建立阶段 4.1协商阶段 4.2认证阶段 4.3 PAP---密…

深入聊聊企业数字化转型这个事儿

01 什么是数字化&#xff1f; 聊数字化&#xff0c;就不得不聊聊信息化、智能化。佛性的说&#xff1a;信息化是数字化的前世&#xff0c;智能化是数字化的来生&#xff01;我习惯用一个结构化的图形来表示事物之间的关系&#xff0c;信息化、数字化、智能化的关系如下&#…

尤大大正式官宣推出VitePress 1.0

VitePress 在现代Web开发领域&#xff0c;构建快速、响应式的文档网站是开发者经常面临的任务之一。VitePress应运而生&#xff0c;它结合了Vue.js和Vite的强大功能&#xff0c;为开发者提供了一个简单易用的静态站点生成器。Vue.js作为一种流行的前端框架&#xff0c;以其简洁…

网络安全:Kali Linux 进行SQL注入与XSS漏洞利用

目录 一、实验 1.环境 2.Kali Linux 进行SQL注入 3.Kali Linux 进行XSS漏洞利用 二、问题 1.XSS分类 2.如何修改beef-xss的密码 3.beef-xss 服务如何管理 4.运行beef报错 5.beef 命令的颜色有哪些区别 6.owasp-top-10 有哪些变化 一、实验 1.环境 &#xff08;1&a…

30---SDRAM电路设计

视频链接 SDRAM电路设计01_哔哩哔哩_bilibili SDRAM电路设计 1、SDRAM简介 SDRAM&#xff1a;Synchronous Dynamic Random Access Memory&#xff0c;同步动态随机存储器。 同步是指其时钟频率和CPU前端总线的系统时钟相同&#xff0c;并且内部命令的发送与数据的传输都以…

如何避免SQL注入攻击?

&#x1f413;序言 当涉及到数据库操作时&#xff0c;防止SQL注入攻击至关重要。SQL注入是一种常见的网络安全威胁&#xff0c;攻击者通过在用户输入中插入恶意的SQL代码&#xff0c;从而可以执行未经授权的数据库操作。 &#x1f413;避免方式 使用参数化查询 使用参数化查询…

【漏洞复现】用友U8 login2.RegisterServlet接口处存在SQL注入漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

MyBatis3源码深度解析(二十五)级联映射与关联查询(二)级联映射的实现原理

文章目录 前言10.2 MyBatis级联映射的实现原理10.2.1 ResultMap详解10.2.2 ResultMap解析过程10.2.3 级联映射的实现原理10.2.3.1 handleRowValuesForSimpleResultMap()10.2.3.2 handleRowValuesForNestedResultMap() 前言 上一节【MyBatis3源码深度解析(二十四)级联映射与关联…

为什么Solana在区块链生态系统中脱颖而出

当我们在不断发展的区块链技术世界中航行时&#xff0c;认识到平台不仅要跟上创新的步伐&#xff0c;还要突破可能的界限&#xff0c;这一点至关重要。#Solana 已成为领先的竞争者&#xff0c;这就是为什么这个高性能区块链的未来看起来很光明。 &#x1f31f; 可扩展性和速度&…

单臂路由和三层交换机

目录 一.单臂路由 1.单臂路由的工作原理 2.单臂路由的配置 2.1画出拓扑图 2.2配置PC 2.3配置交换机 2.4配置路由器 2.5测试 二.三层交换机 1.三层交换机的概述 2.三层交换机的配置 2.1画出拓扑图 2.2配置PC 2.3配置二层交换机 2.4配置三层交换机 2.5测试 3.拓展 三.总结 一.…

Trello国内替代工具有哪些?分享5款

盘点5款类似Trello的本地部署项目管理工具&#xff1a;1.PingCode&#xff1b;2.Worktile&#xff1b;3.Teambition&#xff1b;4.redmine&#xff1b;5.TAIga.io。 Trello是一款杰出的协作与工作管理应用&#xff0c;专为追踪团队项目、凸显当前活动任务、分配责任人&#xff…

web全栈架构师第16期教程

教程介绍 互联网时代已进入后半场&#xff0c;行业环境发生了显著变化。互联网人&#xff0c;尤其是技术人员&#xff0c;如何在加速更迭的技术浪潮中持续充电&#xff0c;提升自身价值&#xff0c;是当下必须面对的挑战。课程涉及了现下前端实际开发时所需要的各块内容&#…

编程语言|C语言——C语言基本数据类型

前言 针对不同的数据&#xff0c;采取不同的存储方式和进行不同的处理。随着处理对象的复杂化&#xff0c;数据类型也要变得更丰富。数据类型的丰富程度直接反映了程序设计语言处理数据的能力。 C语言很重要的一个特点是它的数据类型十分丰富。因此&#xff0c;C语言程序数据处…

求解vue3警告

Invalid prop: type check failed for prop “activeType”. Expected TheTypedFn, got Number with value 0. 警告 复现问题 从demo.vue跳转到after-sale/index.vue页面 //demo.vue <div v-for"(obj, index) in list" :key"index" style"margi…

【Altium】ADTOP层器件如何变更到Bottom层及层颜

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 AD22 PCB TOP层器件变更到Bottom层及Bottom颜色的修改 2、 问题场景 在PCB布局评估设计时&#xff0c;考虑到器件布局摆放的合理性&#xff0c;有些器件需要布局放置在Bottom层&#xff0c;另外根据个人的设计风格习…

【hexo博客6】自定义域名 购买、配置、更新部署

【hexo博客6】自定义域名 购买、配置、更新部署 写在最前面自定义域名购买域名DNS配置Github 配置 更新部署博客 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#…

MySQL的安装(Linux版)

1.所需要的文件 MySQL.zip 2. 卸载自带的Mysql-libs # 查看是否存在 rpm -qa | grep mariadb# 如果存在则执行命令进行卸载 rpm -e --nodeps mariadb-libs3.在/opt目录下创建MySQL目录并上传所需要的安装包 cd /optmkdir MySQL4.按照编号顺序安装&#xff08;压缩包在解压完…

springboot多模块

一、demo 1、创建父项目 首先使用 Spring Initializr 来快速创建好一个Maven工程。然后删除无关的文件&#xff0c;只需保留pom.xml 文件。 &#xff08;1&#xff09;new Project -> spring initializr快速构建SpringBoot&#xff0c;artifactId为springbootmodules&…

手把手教你 - JMeter压力测试

前言 压力测试是每一个Web应用程序上线之前都需要做的一个测试&#xff0c;他可以帮助我们发现系统中的瓶颈问题&#xff0c;减少发布到生产环境后出问题的几率&#xff1b;预估系统的承载能力&#xff0c;使我们能根据其做出一些应对措施。所以压力测试是一个非常重要的步骤&…

文案转化率低?快看看这两个坑你踩没踩

对于很多中小企业来说&#xff0c;无论是来拓市场还是获客&#xff0c;软文营销都会成为他们的主要营销方式&#xff0c;比较软文营销的成本较低&#xff0c;同时门槛也不高。但是也有品牌发现&#xff0c;自己和团队辛苦构思了几周写出的文案&#xff0c;但是效果不是很好。今…