MyBatis之分頁插件(PageHelper)工做原理

  數據分頁功能是咱們軟件系統中必備的功能,在持久層使用mybatis的狀況下,pageHelper來實現後臺分頁則是咱們經常使用的一個選擇,因此本文專門類介紹下。java

PageHelper原理

相關依賴mysql

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.8</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>1.2.15</version>
</dependency>

1.添加plugin

  要使用PageHelper首先在mybatis的全局配置文件中配置。以下:git

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <plugins>
        <!-- com.github.pagehelper爲PageHelper類所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql" />
            <!-- 該參數默認爲false -->
            <!-- 設置爲true時,會將RowBounds第一個參數offset當成pageNum頁碼使用 -->
            <!-- 和startPage中的pageNum效果同樣 -->
            <property name="offsetAsPageNum" value="true" />
            <!-- 該參數默認爲false -->
            <!-- 設置爲true時,使用RowBounds分頁會進行count查詢 -->
            <property name="rowBoundsWithCount" value="true" />
            <!-- 設置爲true時,若是pageSize=0或者RowBounds.limit = 0就會查詢出所有的結果 -->
            <!-- (至關於沒有執行分頁查詢,可是返回結果仍然是Page類型) -->
            <property name="pageSizeZero" value="true" />
            <!-- 3.3.0版本可用 - 分頁參數合理化,默認false禁用 -->
            <!-- 啓用合理化時,若是pageNum<1會查詢第一頁,若是pageNum>pages會查詢最後一頁 -->
            <!-- 禁用合理化時,若是pageNum<1或pageNum>pages會返回空數據 -->
            <property name="reasonable" value="false" />
            <!-- 3.5.0版本可用 - 爲了支持startPage(Object params)方法 -->
            <!-- 增長了一個`params`參數來配置參數映射,用於從Map或ServletRequest中取值 -->
            <!-- 能夠配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默認值 -->
            <!-- 不理解該含義的前提下,不要隨便複製該配置 -->
            <property name="params" value="pageNum=start;pageSize=limit;" />
            <!-- always老是返回PageInfo類型,check檢查返回類型是否爲PageInfo,none返回Page -->
            <property name="returnPageInfo" value="check" />
        </plugin>
    </plugins>
</configuration>

2.加載過程

  咱們經過以下幾行代碼來演示過程github

// 獲取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 經過加載配置文件獲取SqlSessionFactory對象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 獲取SqlSession對象
SqlSession session = factory.openSession();
PageHelper.startPage(1, 5);
session.selectList("com.bobo.UserMapper.query");

加載配置文件咱們從這行代碼開始sql

new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream) {
   return build(inputStream, null, null);
 }

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 獲取到內容:com.github.pagehelper.PageHelper
      String interceptor = child.getStringAttribute("interceptor");
      // 獲取配置的屬性信息
      Properties properties = child.getChildrenAsProperties();
      // 建立的攔截器實例
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      // 將屬性和攔截器綁定
      interceptorInstance.setProperties(properties);
      // 這個方法須要進入查看
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
public void addInterceptor(Interceptor interceptor) {
    // 將攔截器添加到了 攔截器鏈中 而攔截器鏈本質上就是一個List有序集合
    interceptorChain.addInterceptor(interceptor);
  }

在這裏插入圖片描述

小結:經過SqlSessionFactory對象的獲取,咱們加載了全局配置文件及映射文件同時還==將配置的攔截器添加到了攔截器鏈中==。數據庫

3.PageHelper定義的攔截信息

  咱們來看下PageHelper的源代碼的頭部定義安全

@SuppressWarnings("rawtypes")
@Intercepts(
    @Signature(
        type = Executor.class, 
        method = "query", 
        args = {MappedStatement.class
                , Object.class
                , RowBounds.class
                , ResultHandler.class
            }))
public class PageHelper implements Interceptor {
    //sql工具類
    private SqlUtil sqlUtil;
    //屬性參數信息
    private Properties properties;
    //配置對象方式
    private SqlUtilConfig sqlUtilConfig;
    //自動獲取dialect,若是沒有setProperties或setSqlUtilConfig,也能夠正常進行
    private boolean autoDialect = true;
    //運行時自動獲取dialect
    private boolean autoRuntimeDialect;
    //多數據源時,獲取jdbcurl後是否關閉數據源
    private boolean closeConn = true;
// 定義的是攔截 Executor對象中的
// query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)
// 這個方法
type = Executor.class, 
method = "query", 
args = {MappedStatement.class
        , Object.class
        , RowBounds.class
        , ResultHandler.class
    }))

PageHelper中已經定義了該攔截器攔截的方法是什麼。session

4.Executor

  接下來咱們須要分析下SqlSession的實例化過程當中Executor發生了什麼。咱們須要從這行代碼開始跟蹤mybatis

SqlSession session = factory.openSession();
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

加強Executor
在這裏插入圖片描述
在這裏插入圖片描述app

  到此咱們明白了,Executor對象其實被咱們生存的代理類加強了。invoke的代碼爲

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    // 若是是定義的攔截的方法 就執行intercept方法
    if (methods != null && methods.contains(method)) {
      // 進入查看 該方法加強
      return interceptor.intercept(new Invocation(target, method, args));
    }
    // 不是須要攔截的方法 直接執行
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}
/**
 * Mybatis攔截器方法
 *
 * @param invocation 攔截器入參
 * @return 返回執行結果
 * @throws Throwable 拋出異常
 */
public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) {
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}

該方法中的內容咱們後面再分析。Executor的分析咱們到此,接下來看下PageHelper實現分頁的具體過程。

5.分頁過程

  接下來咱們經過代碼跟蹤來看下具體的分頁流程,咱們須要分別從兩行代碼開始:

5.1 startPage

PageHelper.startPage(1, 5);
/**
 * 開始分頁
 *
 * @param params
 */
public static <E> Page<E> startPage(Object params) {
    Page<E> page = SqlUtil.getPageFromObject(params);
    //當已經執行過orderBy的時候
    Page<E> oldPage = SqlUtil.getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    SqlUtil.setLocalPage(page);
    return page;
}
/**
 * 開始分頁
 *
 * @param pageNum    頁碼
 * @param pageSize   每頁顯示數量
 * @param count      是否進行count查詢
 * @param reasonable 分頁合理化,null時用默認配置
 */
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) {
    return startPage(pageNum, pageSize, count, reasonable, null);
}
/**
 * 開始分頁
 *
 * @param offset 頁碼
 * @param limit  每頁顯示數量
 * @param count  是否進行count查詢
 */
public static <E> Page<E> offsetPage(int offset, int limit, boolean count) {
    Page<E> page = new Page<E>(new int[]{offset, limit}, count);
    //當已經執行過orderBy的時候
    Page<E> oldPage = SqlUtil.getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    // 這是重點!!!
    SqlUtil.setLocalPage(page);
    return page;
}
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
// 將分頁信息保存在ThreadLocal中 線程安全!
public static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

5.2selectList方法

session.selectList("com.bobo.UserMapper.query");
public <E> List<E> selectList(String statement) {
  return this.selectList(statement, null);
}

public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

在這裏插入圖片描述
咱們須要回到invoke方法中繼續看

/**
 * Mybatis攔截器方法
 *
 * @param invocation 攔截器入參
 * @return 返回執行結果
 * @throws Throwable 拋出異常
 */
public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) {
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}

進入sqlUtil.processPage(invocation);方法

/**
 * Mybatis攔截器方法
 *
 * @param invocation 攔截器入參
 * @return 返回執行結果
 * @throws Throwable 拋出異常
 */
private Object _processPage(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    Page page = null;
    //支持方法參數時,會先嚐試獲取Page
    if (supportMethodsArguments) {
        // 從線程本地變量中獲取Page信息,就是咱們剛剛設置的
        page = getPage(args);
    }
    //分頁信息
    RowBounds rowBounds = (RowBounds) args[2];
    //支持方法參數時,若是page == null就說明沒有分頁條件,不須要分頁查詢
    if ((supportMethodsArguments && page == null)
            //當不支持分頁參數時,判斷LocalPage和RowBounds判斷是否須要分頁
            || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
        return invocation.proceed();
    } else {
        //不支持分頁參數時,page==null,這裏須要獲取
        if (!supportMethodsArguments && page == null) {
            page = getPage(args);
        }
        // 進入查看
        return doProcessPage(invocation, page, args);
    }
}
/**
  * Mybatis攔截器方法
  *
  * @param invocation 攔截器入參
  * @return 返回執行結果
  * @throws Throwable 拋出異常
  */
 private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
     //保存RowBounds狀態
     RowBounds rowBounds = (RowBounds) args[2];
     //獲取原始的ms
     MappedStatement ms = (MappedStatement) args[0];
     //判斷並處理爲PageSqlSource
     if (!isPageSqlSource(ms)) {
         processMappedStatement(ms);
     }
     //設置當前的parser,後面每次使用前都會set,ThreadLocal的值不會產生不良影響
     ((PageSqlSource)ms.getSqlSource()).setParser(parser);
     try {
         //忽略RowBounds-不然會進行Mybatis自帶的內存分頁
         args[2] = RowBounds.DEFAULT;
         //若是隻進行排序 或 pageSizeZero的判斷
         if (isQueryOnly(page)) {
             return doQueryOnly(page, invocation);
         }

         //簡單的經過total的值來判斷是否進行count查詢
         if (page.isCount()) {
             page.setCountSignal(Boolean.TRUE);
             //替換MS
             args[0] = msCountMap.get(ms.getId());
             //查詢總數
             Object result = invocation.proceed();
             //還原ms
             args[0] = ms;
             //設置總數
             page.setTotal((Integer) ((List) result).get(0));
             if (page.getTotal() == 0) {
                 return page;
             }
         } else {
             page.setTotal(-1l);
         }
         //pageSize>0的時候執行分頁查詢,pageSize<=0的時候不執行至關於可能只返回了一個count
         if (page.getPageSize() > 0 &&
                 ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                         || rowBounds != RowBounds.DEFAULT)) {
             //將參數中的MappedStatement替換爲新的qs
             page.setCountSignal(null);
             // 重點是查看該方法
             BoundSql boundSql = ms.getBoundSql(args[1]);
             args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
             page.setCountSignal(Boolean.FALSE);
             //執行分頁查詢
             Object result = invocation.proceed();
             //獲得處理結果
             page.addAll((List) result);
         }
     } finally {
         ((PageSqlSource)ms.getSqlSource()).removeParser();
     }

     //返回結果
     return page;
 }

進入 BoundSql boundSql = ms.getBoundSql(args[1])方法跟蹤到PageStaticSqlSource類中的

@Override
protected BoundSql getPageBoundSql(Object parameterObject) {
    String tempSql = sql;
    String orderBy = PageHelper.getOrderBy();
    if (orderBy != null) {
        tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
    }
    tempSql = localParser.get().getPageSql(tempSql);
    return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
}

在這裏插入圖片描述

在這裏插入圖片描述

也能夠看Oracle的分頁實現
在這裏插入圖片描述

至此咱們發現PageHelper分頁的實現原來是在咱們執行SQL語句以前動態的將SQL語句拼接了分頁的語句,從而實現了從數據庫中分頁獲取的過程。

相關文章
相關標籤/搜索