1. 程式人生 > >分頁的三種方式

分頁的三種方式

實現方式 優點 缺點 適用場景
subList 簡單、易用 效率低 無法按需批量獲取資料
SQL語句 簡單、直接、效率高 資料庫相容性差 不要求資料庫相容
Hibernate框架 面向物件,相容性強 複雜查詢效能低 相容不同資料庫

一.藉助陣列進行分頁

  • 原理:進行資料庫查詢操作時,獲取到資料庫中所有滿足條件的記錄,儲存在應用的臨時陣列中,再通過List的subList方法,獲取到滿足條件的所有記錄。

  • 實現:

首先在dao層,建立StudentMapper介面,用於對資料庫的操作。在介面中定義通過陣列分頁的查詢方法,如下所示:

List

建立StudentMapper.xml檔案,編寫查詢的sql語句:

 <select id="queryStudentsByArray"  resultMap="studentmapper">
        select * from student
 </select>

可以看出再編寫sql語句的時候,我們並沒有作任何分頁的相關操作。這裡是查詢到所有的學生資訊。

接下來在service層獲取資料並且進行分頁實現:

定義IStuService介面,並且定義分頁方法:

List<Student> queryStudentsByArray(int currPage, int pageSize);

通過接收currPage引數表示顯示第幾頁的資料,pageSize表示每頁顯示的資料條數。

建立IStuService介面實現類StuServiceIml對方法進行實現,對獲取到的陣列通過currPage和pageSize進行分頁:

 @Override
    public List<Student> queryStudentsByArray(int currPage, int pageSize) {
        List<Student> students = studentMapper.queryStudentsByArray();
//        從第幾條資料開始
        int firstIndex = (currPage - 1) * pageSize;
//        到第幾條資料結束
int lastIndex = currPage * pageSize; return students.subList(firstIndex, lastIndex); }

通過subList方法,獲取到兩個索引間的所有資料。

最後在controller中建立測試方法:

  @ResponseBody
    @RequestMapping("/student/array/{currPage}/{pageSize}")
    public List<Student> getStudentByArray(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) {
        List<Student> student = StuServiceIml.queryStudentsByArray(currPage, pageSize);
        return student;
    }

通過使用者傳入的currPage和pageSize獲取指定資料。

二.藉助Sql語句進行分頁

在瞭解到通過陣列分頁的缺陷後,我們發現不能每次都對資料庫中的所有資料都檢索。然後在程式中對獲取到的大量資料進行二次操作,這樣對空間和效能都是極大的損耗。所以我們希望能直接在資料庫語言中只檢索符合條件的記錄,不需要在通過程式對其作處理。這時,Sql語句分頁技術橫空出世。

實現:通過sql語句實現分頁也是非常簡單的,只是需要改變我們查詢的語句就能實現了,即在sql語句後面新增limit分頁語句。

  • 首先還是在StudentMapper介面中新增sql語句查詢的方法,如下:

List

<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
        select * from student limit #{currIndex} , #{pageSize}
</select>

接下來還是在IStuService介面中定義方法,並且在StuServiceIml中對sql分頁實現。

List<Student> queryStudentsBySql(int currPage, int pageSize);
 @Override
    public List<Student> queryStudentsBySql(int currPage, int pageSize) {
        Map<String, Object> data = new HashedMap();
        data.put("currIndex", (currPage-1)*pageSize);
        data.put("pageSize", pageSize);
        return studentMapper.queryStudentsBySql(data);
    }

sql分頁語句如下:select * from table limit index, pageSize;

所以在service中計算出currIndex:要開始查詢的第一條記錄的索引。

三.攔截器分頁

上面提到的陣列分頁和sql語句分頁都不是我們今天講解的重點,今天需要實現的是利用攔截器達到分頁的效果。自定義攔截器實現了攔截所有以ByPage結尾的查詢語句,並且利用獲取到的分頁相關引數統一在sql語句後面加上limit分頁的相關語句,一勞永逸。不再需要在每個語句中單獨去配置分頁相關的引數了。。

首先我們看一下攔截器的具體實現,在這裡我們需要攔截所有以ByPage結尾的所有查詢語句,因此要使用該攔截器實現分頁功能,那麼再定義名稱的時候需要滿足它攔截的規則(以ByPage結尾),

package com.cbg.interceptor;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

/**

  • Created by chenboge on 2017/5/7.
  • description:
    */

/**

  • @Intercepts 說明是一個攔截器
  • @Signature 攔截器的簽名
  • type 攔截的型別 四大物件之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
  • method 攔截的方法
  • args 引數
    */
    @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
    public class MyPageInterceptor implements Interceptor {

//每頁顯示的條目數
private int pageSize;
//當前現實的頁數
private int currPage;

private String dbType;


@Override
public Object intercept(Invocation invocation) throws Throwable {
    //獲取StatementHandler,預設是RoutingStatementHandler
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    //獲取statementHandler包裝類
    MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);

    //分離代理物件鏈
    while (MetaObjectHandler.hasGetter("h")) {
        Object obj = MetaObjectHandler.getValue("h");
        MetaObjectHandler = SystemMetaObject.forObject(obj);
    }

    while (MetaObjectHandler.hasGetter("target")) {
        Object obj = MetaObjectHandler.getValue("target");
        MetaObjectHandler = SystemMetaObject.forObject(obj);
    }

    //獲取連線物件
    //Connection connection = (Connection) invocation.getArgs()[0];


    //object.getValue("delegate");  獲取StatementHandler的實現類

    //獲取查詢介面對映的相關資訊
    MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
    String mapId = mappedStatement.getId();

    //statementHandler.getBoundSql().getParameterObject();

    //攔截以.ByPage結尾的請求,分頁功能的統一實現
    if (mapId.matches(".+ByPage$")) {
        //獲取進行資料庫操作時管理引數的handler
        ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
        //獲取請求時的引數
        Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
        //也可以這樣獲取
        //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();

        //引數名稱和在service中設定到map中的名稱一致
        currPage = (int) paraObject.get("currPage");
        pageSize = (int) paraObject.get("pageSize");

        String sql = (String) MetaObjectHandler.getValue("delegate.boundSql.sql");
        //也可以通過statementHandler直接獲取
        //sql = statementHandler.getBoundSql().getSql();

        //構建分頁功能的sql語句
        String limitSql;
        sql = sql.trim();
        limitSql = sql + " limit " + (currPage - 1) * pageSize + "," + pageSize;

        //將構建完成的分頁sql語句賦值個體'delegate.boundSql.sql',偷天換日
        MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);
    }

    return invocation.proceed();
}


//獲取代理物件
@Override
public Object plugin(Object o) {
    return Plugin.wrap(o, this);
}

//設定代理物件的引數
@Override
public void setProperties(Properties properties) {

//如果專案中分頁的pageSize是統一的,也可以在這裡統一配置和獲取,這樣就不用每次請求都傳遞pageSize引數了。引數是在配置攔截器時配置的。
String limit1 = properties.getProperty("limit", "10");
this.pageSize = Integer.valueOf(limit1);
this.dbType = properties.getProperty("dbType", "mysql");
}
}
上面即是攔截器功能的實現,在intercept方法中獲取到select標籤和sql語句的相關資訊,攔截所有以ByPage結尾的select查詢,並且統一在查詢語句後面新增limit分頁的相關語句,統一實現分頁功能。

重點詳解:

StatementHandler是一個介面,而我們在程式碼中通過StatementHandler statementHandler = (StatementHandler) invocation.getTarget();獲取到的是StatementHandler預設的實現類RoutingStatementHandler。而RoutingStatementHandler只是一箇中間代理,他不會提供具體的方法。那你可能會納悶了,攔截器中基本上是依賴statementHandler獲取各種物件和屬性的,沒有具體屬性和方法怎麼行??接著看下面程式碼:

private final StatementHandler delegate;

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) {
    case 1:
        this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
    case 2:
        this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
    case 3:
        this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
    default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

}

原來它是通過不同的MappedStatement建立不同的StatementHandler實現類物件處理不同的情況。這裡的到的StatementHandler實現類才是真正服務的。看到這裡,你可能就會明白MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");中delegate的來源了吧。至於為什麼要這麼去獲取,後面我們會說道。

拿到statementHandler後,我們會通過MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);去獲取它的包裝物件,通過包裝物件去獲取各種服務。

接下來說說:MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");

上面提到為什麼要這麼去獲取MappedStatement物件??在RoutingStatementHandler中delegate是私有的(private final StatementHandler delegate;),有沒有共有的方法去獲取。所以這裡只有通過反射來獲取啦。

MappedStatement是儲存了xxMapper.xml中一個sql語句節點的所有資訊的包裝類,可以通過它獲取到節點中的所有資訊。在示例中我們拿到了id值,也就是方法的名稱,通過名稱區攔截所有需要分頁的請求。

通過StatementHandler的包裝類,不光能拿到MappedStatement,還可以拿到下面的資料:

public abstract class BaseStatementHandler implements StatementHandler {
protected final Configuration configuration;
protected final ObjectFactory objectFactory;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;
protected final Executor executor;
protected final MappedStatement mappedStatement;
protected final RowBounds rowBounds;
protected BoundSql boundSql;

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    this.objectFactory = this.configuration.getObjectFactory();
    if(boundSql == null) {
        this.generateKeys(parameterObject);
        boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;
    this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql);
}

上面的所有資料都可以通過反射拿到。

幾個重要的引數:
Configuration:所有配置的相關資訊。
ResultSetHandler:用於攔截執行結果的組裝。
ParameterHandler:攔截執行Sql的引數的組裝。
Executor:執行Sql的全過程,包括組裝引數、組裝結果和執行Sql的過程。
BoundSql:執行的Sql的相關資訊。

接下來我們通過如下程式碼拿到請求時的map物件(反射)。

 //獲取進行資料庫操作時管理引數的handler
            ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
            //獲取請求時的引數
            Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
            //也可以這樣獲取
            //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();
拿到我們需要的currPage和pageSize引數後,就是組裝分頁查詢的sql語句’limitSql‘了。

最後通過MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);將原始的sql語句替換成我們新的分頁語句,完成偷天換日的功能,接下來讓程式碼繼續執行。

//編寫好攔截器後,需要註冊到專案中,才能發揮它的作用。在mybatis的配置檔案中,新增如下程式碼:

    <plugins>
        <plugin interceptor="com.cbg.interceptor.MyPageInterceptor">
            <property name="limit" value="10"/>
            <property name="dbType" value="mysql"/>
        </plugin>
    </plugins>

如上所示,還能在裡面配置一些屬性,在攔截器的setProperties方法中可以獲取配置好的屬性值。如專案分頁的pageSize引數的值固定,我們就可以配置在這裡了,以後就不需要每次傳入pageSize了,讀取方式如下:

//讀取配置的代理物件的引數
    @Override
    public void setProperties(Properties properties) {
        String limit1 = properties.getProperty("limit", "10");
        this.pageSize = Integer.valueOf(limit1);
        this.dbType = properties.getProperty("dbType", "mysql");
    }

到這裡,有關攔截器的相關知識就講解的差不多了,接下來就需要測試,是否我們這樣寫真的有效??

首先還是新增dao層的方法和xml檔案的sql語句配置,注意專案中攔截的是以ByPage結尾的請求,所以在這裡,我們的方法名稱也以此結尾:

方法

List<Student> queryStudentsByPage(Map<String,Object> data);

xml檔案的select語句

可以看出,這裡我們就不需要再去手動配置分頁語句了。

接下來是service層的介面編寫和實現方法:

方法:

List<Student> queryStudentsByPage(int currPage,int pageSize);

實現:

 @Override
    public List<Student> queryStudentsByPage(int currPage, int pageSize) {
        Map<String, Object> data = new HashedMap();
        data.put("currPage", currPage);
        data.put("pageSize", pageSize);
        return studentMapper.queryStudentsByPage(data);
    }

這裡我們雖然傳入了currPage和pageSize兩個引數,但是在sql的xml檔案中並沒有使用,直接在攔截器中獲取到統一使用。

最後編寫controller的測試程式碼:

 @ResponseBody
    @RequestMapping("/student/page/{currPage}/{pageSize}")
    public List<Student> getStudentByPage(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) {
        List<Student> student = StuServiceIml.queryStudentsByPage(currPage, pageSize);
        return student;
    }