【12.MyBatis源码剖析与架构实战】19.MyBatis分⻚插件设计与实战
MyBatis 分页插件设计与实战(完整实操案例)
分页查询是业务系统中最常见的需求之一。虽然可以手动在 SQL 后拼接LIMIT或ROWNUM,但这样会侵入业务代码,且需要为每个查询编写重复的分页逻辑。通过 MyBatis 插件机制,我们可以实现一个透明物理分页插件:开发者只需在调用 Mapper 方法前设置分页参数,插件自动拦截 SQL 并改写为对应数据库的分页语句,同时返回总记录数。
一、需求分析
| 功能点 | 说明 |
|---|---|
| 透明分页 | 无需修改原有 Mapper 接口和 XML,只需在 Service 层设置分页参数 |
| 物理分页 | 使用数据库方言(LIMIT/ROWNUM/OFFSET FETCH)实现真分页 |
| 自动查询总数 | 执行分页查询时自动生成COUNT(*)SQL 并查询总记录数 |
| 方言适配 | 支持 MySQL、PostgreSQL、Oracle、SQL Server 等 |
| 返回结果 | 返回包含分页信息(总条数、当前页、每页大小)和数据的统一对象 |
二、设计思路
基于 MyBatis 插件机制,拦截Executor.query方法(因为所有查询最终都会经过它)。核心步骤如下:
- 定义分页参数对象
Page:存储当前页码、每页大小、总记录数、排序字段等。 - 定义方言接口
Dialect:提供getCountSql(生成计数 SQL)和getPageSql(生成分页 SQL)方法。 - 实现各数据库方言:MySQL、Oracle、PostgreSQL 等。
- 编写分页拦截器
PageInterceptor:- 通过
@Intercepts注解拦截Executor.query方法。 - 判断参数中是否包含
Page对象,若无则直接放行。 - 若有,则:
- 通过反射获取原始的
MappedStatement和BoundSql。 - 生成并执行 COUNT SQL,获取总记录数并设置到
Page对象中。 - 生成分页 SQL,替换原有
BoundSql中的 SQL,并继续执行查询。 - 将查询结果封装到
Page对象中返回。
- 通过反射获取原始的
- 通过
- 使用
ThreadLocal传递分页参数:实现线程安全的分页参数传递,避免侵入方法签名。
三、完整源码实现
3.1 分页参数对象Page.java
packagecom.example.plugin.page;importjava.io.Serializable;importjava.util.List;publicclassPage<T>implementsSerializable{privatestaticfinallongserialVersionUID=1L;// 当前页码privateintpageNum=1;// 每页条数privateintpageSize=10;// 总记录数privatelongtotal=0;// 总页数privateintpages=0;// 查询结果集privateList<T>list;// 是否查询总数(默认 true)privatebooleancount=true;// 排序字段(可选,用于简化)privateStringorderBy;publicPage(){}publicPage(intpageNum,intpageSize){this.pageNum=pageNum;this.pageSize=pageSize;}// getter / setter 省略}3.2 方言接口Dialect.java
packagecom.example.plugin.dialect;publicinterfaceDialect{/** * 根据原始 SQL 生成计数 SQL * @param originalSql 原始查询 SQL * @return 计数 SQL */StringgetCountSql(StringoriginalSql);/** * 根据原始 SQL 和分页参数生成分页 SQL * @param originalSql 原始查询 SQL * @param offset 起始行(从0开始) * @param limit 每页条数 * @return 分页 SQL */StringgetPageSql(StringoriginalSql,intoffset,intlimit);}3.3 MySQL 方言实现MySQLDialect.java
packagecom.example.plugin.dialect;publicclassMySQLDialectimplementsDialect{@OverridepublicStringgetCountSql(StringoriginalSql){// 简单的 SELECT COUNT(*) 包装,实际需处理 ORDER BY、DISTINCT 等return"SELECT COUNT(*) FROM ("+originalSql+") AS tmp_count";}@OverridepublicStringgetPageSql(StringoriginalSql,intoffset,intlimit){returnoriginalSql+" LIMIT "+offset+", "+limit;}}3.4 Oracle 方言实现OracleDialect.java
packagecom.example.plugin.dialect;publicclassOracleDialectimplementsDialect{@OverridepublicStringgetCountSql(StringoriginalSql){return"SELECT COUNT(*) FROM ("+originalSql+")";}@OverridepublicStringgetPageSql(StringoriginalSql,intoffset,intlimit){intstartRow=offset+1;intendRow=offset+limit;StringBuildersql=newStringBuilder();sql.append("SELECT * FROM (SELECT TMP_PAGE.*, ROWNUM PAGE_ROW_NUM FROM (");sql.append(originalSql);sql.append(") TMP_PAGE WHERE ROWNUM <= ").append