MyBatis分頁功能的實現(陣列分頁、sql分頁、攔截器,RowBounds分頁)
前言:學習hibernate & mybatis等持久層框架的時候,不外乎對資料庫的增刪改查操作。而使用最多的當是資料庫的查詢操作, 而當資料庫資料過多時,符合查詢條件的資料可能也會是很龐大的資料。往往在這個時候,我們都不會希望一次性的將所有的資料一起性讀取出來,並且顯示在UI介面上。常用的操作,就是對查詢到的資料進行分頁,每次處理小部分資料。這樣每次處理的資料量就會在可控的範圍,UI的展示也會很協調。
問題:面對上面的問題,今天我們就來進行基於mybatis和MySql進行分頁功能的實現。常見的資料分頁有哪幾種實現??基於陣列的分頁實現?基於sql語句的分頁實現?還是通過攔截器進行資料分頁功能?還是通過RowBounds引數進行物理分頁?幾種都是常用的分頁實現原理,接下來就按照陣列、sql語句,攔截器和RowBounds的方式介紹分頁功能。
一.藉助陣列進行分頁
原理:進行資料庫查詢操作時,獲取到資料庫中所有滿足條件的記錄,儲存在應用的臨時陣列中,再通過List的subList方法,獲取到滿足條件的所有記錄。
實現:
首先在dao層,建立StudentMapper介面,用於對資料庫的操作。在介面中定義通過陣列分頁的查詢方法,如下所示:
List<Student> queryStudentsByArray();
- 1
方法很簡單,就是獲取所有的資料,通過list接收後進行分頁操作。
建立StudentMapper.xml檔案,編寫查詢的sql語句:
<select id="queryStudentsByArray" resultMap="studentmapper">
select * from student
</select>
- 1
- 2
- 3
可以看出再編寫sql語句的時候,我們並沒有作任何分頁的相關操作。這裡是查詢到所有的學生資訊。
接下來在service層獲取資料並且進行分頁實現:
定義IStuService介面,並且定義分頁方法:
List<Student> queryStudentsByArray(int currPage, int pageSize);
- 1
通過接收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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
通過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;
}
- 1
- 2
- 3
- 4
- 5
- 6
通過使用者傳入的currPage和pageSize獲取指定資料。
測試:
首先我們來獲取再沒實現分頁效果前獲取到的所有資料,如下所示:
接下來在瀏覽器輸入http://localhost:8080/student/student/array/1/2
測試實現了分頁後的資料。獲取第一頁的資料,每頁顯示兩條資料。
結果如下: 輸出的是指定的從第0-2條資料,可見我們通過陣列分頁的功能是成功的。(這裡因為用到了關聯查詢,所以看起來資料可能比較多)
缺點:資料庫查詢並返回所有的資料,而我們需要的只是極少數符合要求的資料。當資料量少時,還可以接受。當資料庫資料量過大時,每次查詢對資料庫和程式的效能都會產生極大的影響。
二.藉助Sql語句進行分頁
在瞭解到通過陣列分頁的缺陷後,我們發現不能每次都對資料庫中的所有資料都檢索。然後在程式中對獲取到的大量資料進行二次操作,這樣對空間和效能都是極大的損耗。所以我們希望能直接在資料庫語言中只檢索符合條件的記錄,不需要在通過程式對其作處理。這時,Sql語句分頁技術橫空出世。
實現:通過sql語句實現分頁也是非常簡單的,只是需要改變我們查詢的語句就能實現了,即在sql語句後面新增limit分頁語句。
首先還是在StudentMapper介面中新增sql語句查詢的方法,如下:
List<Student> queryStudentsBySql(Map<String,Object> data);
- 1
然後在StudentMapper.xml檔案中編寫sql語句通過limiy關鍵字進行分頁:
<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
select * from student limit #{currIndex} , #{pageSize}
</select>
- 1
- 2
- 3
接下來還是在IStuService介面中定義方法,並且在StuServiceIml中對sql分頁實現。
List<Student> queryStudentsBySql(int currPage, int pageSize);
- 1
@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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
sql分頁語句如下:select * from table limit index, pageSize;
所以在service中計算出currIndex:要開始查詢的第一條記錄的索引。
測試:
在瀏覽器輸入http://localhost:8080/student/student/sql/1/2
獲取第一頁的資料,每頁顯示兩條資料。
結果: 從輸出結果可以看出和陣列分頁的結果是一致的,因此sql語句的分頁也是沒問題的。
缺點:雖然這裡實現了按需查詢,每次檢索得到的是指定的資料。但是每次在分頁的時候都需要去編寫limit語句,很冗餘。而且不方便統一管理,維護性較差。所以我們希望能夠有一種更方便的分頁實現。
三.攔截器分頁
上面提到的陣列分頁和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.
* <p>
* Email:[email protected]
* <p>
* description:
*/
/**
* @Intercepts 說明是一個攔截器
* @Signature 攔截器的簽名
* type 攔截的型別 四大物件之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
* method 攔截的方法
* args 引數
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public classMyPageInterceptorimplementsInterceptor {
//每頁顯示的條目數
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) {
//生成object物件的動態代理物件
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");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
上面即是攔截器功能的實現,在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());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
原來它是通過不同的MappedStatement建立不同的StatementHandler實現類物件處理不同的情況。這裡的到的StatementHandler實現類才是真正服務的。看到這裡,你可能就會明白MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
中delegate的來源了吧。至於為什麼要這麼去獲取,後面我們會說道。
拿到statementHandler後,我們會通過MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);
去獲取它的包裝物件,通過包裝物件去獲取各種服務。
MetaObject:mybatis的一個工具類,方便我們有效的讀取或修改一些重要物件的屬性。四大物件(ResultSetHandler,ParameterHandler,Executor和statementHandler)提供的公共方法很少,要想直接獲取裡面屬性的值很困難,但是可以通過MetaObject利用一些技術(內部反射實現)很輕鬆的讀取或修改裡面的資料。
接下來說說:MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
上面提到為什麼要這麼去獲取MappedStatement物件??在RoutingStatementHandler中delegate是私有的(private final StatementHandler delegate;
),有沒有共有的方法去獲取。所以這裡只有通過反射來獲取啦。
MappedStatement是儲存了xxMapper.xml中一個sql語句節點的所有資訊的包裝類,可以通過它獲取到節點中的所有資訊。在示例中我們拿到了id值,也就是方法的名稱,通過名稱區攔截所有需要分頁的請求。
通過StatementHandler的包裝類,不光能拿到MappedStatement,還可以拿到下面的資料:
public abstract classBaseStatementHandlerimplementsStatementHandler {
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
上面的所有資料都可以通過反射拿到。
幾個重要的引數: 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();
- 1
- 2
- 3
- 4
- 5
- 6
拿到我們需要的currPage和pageSize引數後,就是組裝分頁查詢的sql語句’limitSql‘了。
最後通過MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);
將原始的sql語句替換成我們新的分頁語句,完成偷天換日的功能,接下來讓程式碼繼續執行。
編寫好攔截器後,需要註冊到專案中,才能發揮它的作用。在mybatis的配置檔案中,新增如下程式碼:
<plugins>
<plugininterceptor="com.cbg.interceptor.MyPageInterceptor">
<propertyname="limit"value="10"/>
<propertyname="dbType"value="mysql"/>
</plugin>
</plugins>
- 1
- 2
- 3
- 4
- 5
- 6
如上所示,還能在裡面配置一些屬性,在攔截器的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");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
到這裡,有關攔截器的相關知識就講解的差不多了,接下來就需要測試,是否我們這樣寫真的有效??
首先還是新增dao層的方法和xml檔案的sql語句配置,注意專案中攔截的是以ByPage結尾的請求,所以在這裡,我們的方法名稱也以此結尾:
方法
List<Student> queryStudentsByPage(Map<String,Object> data);
xml檔案的select語句
<select id="queryStudentsByPage" parameterType="map" resultMap="studentmapper">
select * from student
</select>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
可以看出,這裡我們就不需要再去手動配置分頁語句了。
接下來是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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
這裡我們雖然傳入了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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
測試:
在瀏覽器輸入:http://localhost:8080/student/student/page/1/2
結果: 可見和上面兩種分頁的效果是一樣的。
四.RowBounds實現分頁
原理:通過RowBounds實現分頁和通過陣列方式分頁原理差不多,都是一次獲取所有符合條件的資料,然後在記憶體中對大資料進行操作,實現分頁效果。只是陣列分頁需要我們自己去實現分頁邏輯,這裡更加簡化而已。
存在問題:一次性從資料庫獲取的資料可能會很多,對記憶體的消耗很大,可能導師效能變差,甚至引發記憶體溢位。
適用場景:在資料量很大的情況下,建議還是適用攔截器實現分頁效果。RowBounds建議在資料量相對較小的情況下使用。
簡單介紹:這是程式碼實現上最簡單的一種分頁方式,只需要在dao層介面中要實現分頁的方法中加入RowBounds引數,然後在service層通過offset(從第幾行開始讀取資料,預設值為0)和limit(要顯示的記錄條數,預設為java允許的最大整數:2147483647)兩個引數構建出RowBounds物件,在呼叫dao層方法的時,將構造好的RowBounds傳進去就能輕鬆實現分頁效果了。
具體操作如下:
dao層介面方法:
//加入RowBounds引數
public List<UserBean> queryUsersByPage(String userName, RowBounds rowBounds);
- 1
- 2
然後在service層構建RowBounds,呼叫dao層方法:
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.SUPPORTS)
public List<RoleBean> queryRolesByPage(String roleName, int start, int limit) {
return roleDao.queryRolesByPage(roleName, new RowBounds(start, limit));
}
- 1
- 2
- 3
- 4
- 5
RowBounds就是一個封裝了offset和limit簡單類,如下所示:
public classRowBounds {
public static final int NO_ROW_OFFSET = 0;
public static final int NO_ROW_LIMIT = 2147483647;
public static final RowBounds DEFAULT = new RowBounds();
private int offset;
private int limit;
public RowBounds() {
this.offset = 0;
this.limit = 2147483647;
}
public RowBounds(int offset, int limit) {
this.offset = offset;
this.limit = limit;
}
public int getOffset() {
return this.offset;
}
public int getLimit() {
return this.limit;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
只需要這兩步操作,就能輕鬆實現分頁效果了,是不是很神奇。但卻不簡單,內部是怎麼實現的??給大家提供一個簡單的思路:RowBounds分頁簡單原理
結論:從上面四種sql分頁的實現方式可以看出,通過RowBounds實現是最簡便的,但是通過攔截器的實現方式是最優的方案。只需一次編寫,所有的分頁方法共同使用,還可以避免多次配置時的出錯機率,需要修改時也只需要修改這一個檔案,一勞永逸。而且是我們自己實現的,便於我們去控制和增加一些邏輯處理,使我們在外層更簡單的使用。同時也不會出現陣列分頁和RowBounds分頁導致的效能問題。當然,具體情況可以採取不同的解決方案。資料量小時,RowBounds不失為一種好辦法。但是資料量大時,實現攔截器就很有必要了。
到這裡,mybatis的分頁原理和全部實現過程都完成了,還有不清楚的可以自己去看一下mybatis的原始碼,按照這個思路去閱讀還是比較清晰的。這裡只是對外掛(攔截器)實現分頁做了個簡單的介紹,只是簡單的分頁功能,還很簡陋。在下一遍部落格我們將會實現一個封裝好的、功能齊全的實用性外掛。傳送門:mybatis精通之路之外掛分頁(攔截器)進階 轉載地址:https://blog.csdn.net/chenbaige/article/details/70846902