Spring Boot从入门到进阶教程系列 -- MyBatis分页拦截器实现(包含数据库方言)

本教程详细介绍了如何在Spring Boot中使用MyBatis实现分页拦截器,包括编写分页对象接口、实现类,利用MyBatis的拦截器机制,以及数据库方言的支持。通过在DruidConfiguration中配置MyBatis plugins,可以扩展更多的自定义拦截器,如性能监控和异常捕获等。
上一个教程我们讲解如何配置MyBatis实例化,数据库类型选择器,数据库方言实现,本次我们将接着上个教程的成果来整合MyBatis常用的分页拦截器实现,如需看上篇教程的同学可点击链接

【Spring Boot从入门到进阶教程系列 -- MyBatis配置,数据库选择器和分页方言实现】 

下面我们直接开启代码之旅

步骤1. 编写我们的分页对象接口

public interface Pagination<T> extends Serializable {

    public boolean isSpilled();

    public void setSpilled(boolean spilled);

    public Integer getPageNo();

    public void setPageNo(Integer pageNo);

    public Integer getPageSize();

    public void setPageSize(Integer pageSize);

    public Integer getPageTotal();

    public void setPageTotal(Integer pageTotal);

    public Integer getPageNumber();

    public void setPageNumber(Integer pageNumber);

    public List<T> getData();

    public void setData(List<T> data);

}


步骤2. 编写分页对象接口实现类

public class SimplePagination<T> implements Pagination<T> {

    private boolean spilled = true; // 是否重置溢出的PageNo
    private Integer pageNo;
    private Integer pageSize;
    private Integer pageTotal;
    private Integer pageNumber;

    private List<T> data;

    public SimplePagination() {

    }

    public SimplePagination(Integer pageNo, Integer pageSize, boolean spilled) {
        this(pageNo, pageSize, 0, null, null);
        this.spilled = spilled;
    }

    public SimplePagination(Integer pageNo, Integer pageSize) {
        this(pageNo, pageSize, 0, null, null);
    }

    public SimplePagination(Integer pageNo, Integer pageSize, Integer pageTotal) {
        this(pageNo, pageSize, pageTotal, (pageTotal % pageSize == 0) ? (pageTotal / pageSize) : (pageTotal / pageSize + 1), null);
    }

    public SimplePagination(Integer pageNo, Integer pageSize, Integer pageTotal, List<T> data) {
        this(pageNo, pageSize, pageTotal, (pageTotal % pageSize == 0) ? (pageTotal / pageSize) : (pageTotal / pageSize + 1), data);
    }

    public SimplePagination(Integer pageNo, Integer pageSize, Integer pageTotal, Integer pageNumber, List<T> data) {
        this.pageNo = pageNo;
        this.pageSize = pageSize;
        this.pageTotal = pageTotal;
        this.pageNumber = pageNumber;
        this.data = data;
    }

    public boolean isSpilled() {
        return spilled;
    }

    public 
<think> 我们正在讨论如何使用MyBatis拦截器实现分页功能。根据用户之前的对话,他们提到了MyBatis-Flex的分页实现,但现在的问题是直接关于MyBatis拦截器实现分页。 引用[1]提到:Mybatis拦截器可以用来自定义分页功能,数据权限管理,监控sql执行性能等。并给出了Interceptor接口的定义。 引用[2]:在SpringBoot项目中,可以通过配置DataSourceConfig来安装拦截器,并提到需要Dialect(数据库方言)的支持。 引用[3]:传统的Mybatis分页需要在每个查询语句中硬编码limit,而使用拦截器可以避免这种情况。 引用[4]:展示了一个理想的使用方式,通过一个工具类(MagicPage)来开始分页,然后拦截器自动处理,最后获取分页信息。 因此,我们的回答应该围绕如何通过自定义拦截器实现分页,避免在每个SQL中写分页语句。 步骤: 1. 创建分页参数类(PageInfo)和分页工具类(PageHelper)用于存储分页信息(当前页码、每页数量等)和提供静态方法。 2. 自定义拦截器(PaginationInterceptor),实现Mybatis的Interceptor接口。 3. 在拦截器的intercept方法中,判断是否有分页参数,如果有则修改原SQL为分页SQL(需要根据不同的数据库方言生成)。 4. 在执行查询后,获取总记录数(可能需要执行count查询)并设置到分页对象中。 5. 配置拦截器(在mybatis-config.xml中或者通过Spring Boot配置)。 注意:引用[4]中展示了一个使用示例,我们可以参考这个示例来设计。 具体实现步骤: 第一步:定义分页参数类 ```java public class PageInfo { private int pageNum; // 当前页 private int pageSize; // 每页条数 private long total; // 总记录数 private List<?> list; // 当前页数据 // 省略getter/setter } ``` 第二步:定义分页工具类(用于设置分页参数和获取结果) ```java public class PageHelper { private static final ThreadLocal<PageInfo> PAGE_INFO = new ThreadLocal<>(); public static void startPage(int pageNum, int pageSize) { PageInfo pageInfo = new PageInfo(); pageInfo.setPageNum(pageNum); pageInfo.setPageSize(pageSize); PAGE_INFO.set(pageInfo); } public static PageInfo getPageInfo() { return PAGE_INFO.get(); } public static void clearPage() { PAGE_INFO.remove(); } } ``` 第三步:自定义拦截器(PaginationInterceptor) 我们需要实现Interceptor接口,并在intercept方法中完成以下操作: 1. 获取当前的PageInfo(通过PageHelper.getPageInfo()),如果没有则说明不需要分页。 2. 获取原始的SQL和参数。 3. 执行一个count查询(获取总记录数)。 4. 修改原始SQL,根据数据库方言添加分页语句(比如MySQL的LIMIT,Oracle的ROWNUM等)。 5. 执行分页查询,并设置结果到PageInfo中。 6. 清理ThreadLocal中的分页参数。 代码示例: ```java @Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})) public class PaginationInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取当前的PageInfo PageInfo pageInfo = PageHelper.getPageInfo(); if (pageInfo == null) { return invocation.proceed(); } // 获取StatementHandler StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); // 获取原始SQL String originalSql = boundSql.getSql(); // 获取连接 Connection connection = (Connection) invocation.getArgs()[0]; // 执行count查询获取总记录数 long total = getTotal(statementHandler, connection, originalSql); pageInfo.setTotal(total); // 修改原始SQL为分页SQL(这里以MySQL为例) String pageSql = getPageSql(originalSql, pageInfo); // 通过反射修改boundSql中的sql Field field = BoundSql.class.getDeclaredField("sql"); field.setAccessible(true); field.set(boundSql, pageSql); // 继续执行查询 try { return invocation.proceed(); } finally { // 清理分页参数 PageHelper.clearPage(); } } private long getTotal(StatementHandler statementHandler, Connection connection, String originalSql) throws SQLException { // 构建count查询的SQL String countSql = "SELECT COUNT(*) FROM (" + originalSql + ") AS total"; try (PreparedStatement countStmt = connection.prepareStatement(countSql); ResultSet rs = countStmt.executeQuery()) { if (rs.next()) { return rs.getLong(1); } } return 0; } private String getPageSql(String sql, PageInfo pageInfo) { int offset = (pageInfo.getPageNum() - 1) * pageInfo.getPageSize(); return sql + " LIMIT " + offset + ", " + pageInfo.getPageSize(); } // 其他方法:plugin和setProperties @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } } ``` 第四步:配置拦截器Spring Boot中,可以通过配置类来添加拦截器: ```java @Configuration public class MyBatisConfig { @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } } ``` 或者在mybatis-config.xml中配置: ```xml <plugins> <plugin interceptor="com.example.interceptor.PaginationInterceptor"></plugin> </plugins> ``` 第五步:使用示例 ```java // 在需要分页的查询前调用 PageHelper.startPage(1, 10); // 第1页,每页10条 List<User> users = userMapper.selectAll(); PageInfo pageInfo = PageHelper.getPageInfo(); // 此时users实际上是当前页的数据,而pageInfo包含了总记录数等信息 ``` 注意:以上代码是简化版,实际应用中需要考虑: 1. 不同的数据库方言(MySQL, Oracle, PostgreSQL等)分页语句不同,可以通过Dialect抽象。 2. 可能需要对查询语句进行优化,例如去掉order by子句用于count查询(但要注意有些情况不能去掉)。 3. 线程安全:使用ThreadLocal保存分页参数,确保多线程下不会错乱。 4. 性能:对于复杂的SQL,count查询可能较慢,可以考虑缓存等优化。 引用[2]中提到了DataSourceConfig配置拦截器,以及需要Dialect(方言)支持,我们在上面的拦截器中只实现了MySQL的分页,可以改进为支持多种数据库。 改进思路:定义一个Dialect接口,然后为每种数据库实现不同的分页逻辑。 相关问题: 1. 如何支持多种数据库分页方言? 2. 分页拦截器对性能的影响有哪些?如何优化? 3. 分页查询中如何处理结果嵌套(一对多)的情况? 根据上述内容,整理回答。 </think> ### 使用 MyBatis 拦截器实现分页功能 通过自定义拦截器实现分页功能,可避免在每个 SQL 中硬编码分页逻辑,实现过程分为以下步骤: --- #### 一、核心实现步骤 1. **定义分页工具类** 用于存储分页参数(当前页/每页条数)和分页结果: ```java public class PageHelper { private static final ThreadLocal<PageInfo> PAGE_INFO = new ThreadLocal<>(); // 启动分页(pageNum=页码, pageSize=每页条数) public static void startPage(int pageNum, int pageSize) { PageInfo pageInfo = new PageInfo(pageNum, pageSize); PAGE_INFO.set(pageInfo); } // 获取分页结果 public static PageInfo getPageInfo() { return PAGE_INFO.get(); } // 清理ThreadLocal public static void clearPage() { PAGE_INFO.remove(); } } public class PageInfo { private int pageNum; private int pageSize; private long total; private List<?> data; // 构造方法/getters/setters } ``` 2. **实现分页拦截器** 拦截查询请求并动态修改 SQL: ```java @Intercepts(@Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} )) public class PaginationInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 检查是否启用分页 PageInfo pageInfo = PageHelper.getPageInfo(); if (pageInfo == null) { return invocation.proceed(); } StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); // 1. 执行COUNT查询获取总记录数 long total = executeCountQuery(invocation, boundSql); pageInfo.setTotal(total); // 2. 修改原SQL为分页SQL String originalSql = boundSql.getSql(); String pageSql = buildPageSql(originalSql, pageInfo); // 根据数据库方言生成 // 反射修改SQL语句 Field sqlField = BoundSql.class.getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, pageSql); return invocation.proceed(); } private long executeCountQuery(Invocation invocation, BoundSql boundSql) throws SQLException { // 构建COUNT语句: SELECT COUNT(*) FROM (原SQL) String countSql = "SELECT COUNT(*) FROM (" + boundSql.getSql() + ") tmp"; // 执行COUNT查询并返回结果(需复用原参数) // ... 具体实现参考[^2] } private String buildPageSql(String sql, PageInfo pageInfo) { // MySQL示例: offset = (pageNum-1)*pageSize int offset = (pageInfo.getPageNum() - 1) * pageInfo.getPageSize(); return sql + " LIMIT " + offset + ", " + pageInfo.getPageSize(); // Oracle/PostgreSQL等需适配不同方言[^2] } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) {} } ``` **关键点**: - 通过 `ThreadLocal` 保证分页参数线程安全 - 使用反射修改 `BoundSql` 中的 SQL 语句 - 根据数据库类型动态生成分页 SQL [^2] 3. **注册拦截器** - **XML 配置** (`mybatis-config.xml`): ```xml <plugins> <plugin interceptor="com.example.PaginationInterceptor"/> </plugins> ``` - **Spring Boot 配置**: ```java @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } ``` --- #### 二、使用示例 ```java // 1. 启动分页(第2页,每页10条) PageHelper.startPage(2, 10); // 2. 执行查询(自动被拦截器修改) List<User> users = userMapper.selectAll(); // 3. 获取分页结果 PageInfo pageInfo = PageHelper.getPageInfo(); System.out.println("总记录数: " + pageInfo.getTotal()); System.out.println("当前页数据: " + pageInfo.getData()); // 4. 清理分页参数 PageHelper.clearPage(); ``` --- #### 三、优化与注意事项 1. **数据库方言适配** 建议抽象 `Dialect` 接口处理不同数据库分页语法: ```java public interface Dialect { String buildPageSql(String originSql, int offset, int limit); } // 实现MySQLDialect/OracleDialect等[^2] ``` 2. **性能优化** - 当已知总记录数时跳过 COUNT 查询: ```java PageHelper.startPage(2, 10).setSearchCount(false); ``` - 复杂 SQL 的 COUNT 查询可优化为简单查询[^1] 3. **规避问题** - **SQL 注入风险**:避免直接拼接参数,应使用 `PreparedStatement` - **嵌套查询**:拦截器需跳过存储过程等特殊语句(检查 `MappedStatement` 类型) - **延迟加载**:与 MyBatis 懒加载兼容性需测试[^4] --- #### 四、与传统分页对比 | **方式** | 硬编码分页 | 拦截器分页 | |------------------|-------------------------------|-------------------------------| | **SQL 修改** | 每个 SQL 添加 `LIMIT` | 无需修改 SQL | | **代码侵入性** | 高 | 低(集中处理) | | **多数据库支持** | 需手动适配 | 统一拦截器适配 | | **维护成本** | 修改每个分页查询 | 仅维护拦截器 | > 💡 **最佳实践**:在复杂查询场景(如多表关联)中优先使用拦截器分页,避免业务代码渗透 SQL 逻辑[^3]。 --- ####
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值