首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

MyBatis 分頁攔截器深度解析:原理實現與多數據庫適配

     編輯:Java 相關知識

前言

分頁查詢是企業級應用的高頻需求。MyBatis 通過攔截器機制實現無侵入式分頁,本文深度解析 PageInterceptor 的核心原理、MySQL/Oracle 方言適配、總記錄數統計優化及 Spring 整合配置全流程。

核心架構設計

攔截器鏈路圖解


SQL 執行流程:
StatementHandler.prepare() 
    → RoutingStatementHandler 
        → BaseStatementHandler (Simple/Prepared/Callable)
            → 實際 SQL 執行

攔截點精準鎖定 StatementHandler.prepare(Connection) 方法,在 SQL 預編譯前完成分頁改寫。

關鍵類結構

  • PageInterceptor:實現 Interceptor 接口,核心邏輯載體
  • Page:分頁參數載體(pageNo、pageSize、totalRecord)
  • ReflectHelper:反射工具類,突破私有字段訪問限制

核心源碼解析

1. SQL 改寫入口

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class PageInterceptor implements Interceptor {
    
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 獲取 RoutingStatementHandler
        RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        
        // 2. 反射獲取委託對象
        StatementHandler delegate = (StatementHandler) ReflectHelper.getFieldValue(handler, "delegate");
        BoundSql boundSql = delegate.getBoundSql();
        
        // 3. 參數校驗:必須包含 Page 對象
        Object paramObj = boundSql.getParameterObject();
        if (paramObj instanceof Page) {
            Page page = (Page) paramObj;
            
            // 4. 獲取 MappedStatement(包含原始 SQL)
            MappedStatement ms = (MappedStatement) ReflectHelper.getFieldValue(delegate, "mappedStatement");
            Connection conn = (Connection) invocation.getArgs()[0];
            String originalSql = boundSql.getSql();
            
            // 5. 統計總記錄數(關鍵優化點)
            setTotalRecord(page, ms, conn);
            
            // 6. 生成分頁 SQL
            String pageSql = getPageSql(page, originalSql);
            
            // 7. 反射修改 BoundSql 中的 SQL
            ReflectHelper.setFieldValue(boundSql, "sql", pageSql);
        }
        return invocation.proceed();
    }
}

2. 總記錄數統計優化

private void setTotalRecord(Page page, MappedStatement ms, Connection conn) {
    BoundSql boundSql = ms.getBoundSql(page);
    String originalSql = boundSql.getSql();
    
    // 智能生成 COUNT SQL:從 FROM 關鍵字截取
    String countSql = "SELECT COUNT(*) " + originalSql.substring(originalSql.toLowerCase().indexOf("from"));
    
    // 復用參數映射關係
    List mappings = boundSql.getParameterMappings();
    BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, mappings, page);
    
    // 使用 MyBatis 內建參數處理器
    ParameterHandler ph = new DefaultParameterHandler(ms, page, countBoundSql);
    
    try (PreparedStatement ps = conn.prepareStatement(countSql)) {
        ph.setParameters(ps);
        try (ResultSet rs = ps.executeQuery()) {
            if (rs.next()) page.setTotalRecord(rs.getInt(1));
        }
    }
}

3. 多數據庫方言適配

private String getPageSql(Page page, String sql) {
    StringBuffer sb = new StringBuffer(sql);
    
    if ("mysql".equalsIgnoreCase(dialect)) {
        // MySQL: LIMIT offset, size
        int offset = (page.getPage() - 1) * page.getRows();
        return sb.append(" LIMIT ").append(offset).append(",").append(page.getRows()).toString();
    } 
    else if ("oracle".equalsIgnoreCase(dialect)) {
        // Oracle: 雙層 ROWNUM 嵌套
        int offset = (page.getPage() - 1) * page.getRows() + 1;
        int end = offset + page.getRows() - 1;
        
        sb.insert(0, "SELECT u.*, ROWNUM r FROM (").append(") u WHERE ROWNUM <= ").append(end);
        sb.insert(0, "SELECT * FROM (").append(") WHERE r >= ").append(offset);
        return sb.toString();
    }
    return sb.toString();
}

Spring 整合配置

Mapper 掃描與攔截器註冊

<!-- Mapper 掃描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.yidao.mybatis.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

<!-- 分頁攔截器 -->
<bean id="paginationInterceptor" class="com.mybatis.interceptor.PageInterceptor">
    <property name="dialect" value="mysql"/>
    <!-- 正則匹配需分頁的 Statement ID -->
    <property name="pageSqlId" value=".*query$"/>
</bean>

SqlSessionFactory 整合攔截器

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="plugins">
        <array>
            <ref bean="paginationInterceptor"/>
        </array>
    </property>
</bean>

使用示例

Mapper 接口定義

public interface UserMapper {
    // 方法名匹配 pageSqlId 正則
    List queryUserList(Page page, @Param("name") String name);
}

XML 映射文件

<select id="queryUserList" resultType="User" parameterType="Page">
    SELECT * FROM t_user 
    <where>
        <if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
    </where>
</select>

Service 層調用

public Page getUsers(int pageNo, int pageSize, String name) {
    Page page = new Page<>();
    page.setPage(pageNo);
    page.setRows(pageSize);
    
    // MyBatis 自動填充 totalRecord 並改寫 SQL
    List list = userMapper.queryUserList(page, name);
    page.setResults(list);
    return page;
}

進階優化建議

  • Count SQL 緩存:對相同原始 SQL 緩存生成的 Count SQL,避免重複字符串操作
  • 物理分頁下推:針對複雜關聯查詢,建議手寫 Count SQL 避免全表掃描
  • 參數類型擴展:支持 Map、POJO 等多種參數載體,不強制要求 Page 類型
  • 方言擴展機制:採用策略模式封裝 Dialect 接口,新增 PostgreSQL/SQLServer 等數據庫零修改核心代碼

總結

MyBatis 分頁攔截器通過「反射突破封裝 + SQL 動態改寫 + 參數映射復用」三大技術,實現了零業務代碼侵入的通用分頁方案。掌握其核心原理,可根據業務場景進行深度定製,滿足高性能、多數據庫、複雜查詢等進階需求。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved