一.借助數組進行分頁
原理:進行數據庫查詢操作時,獲取到數據庫中所有滿足條件的記錄,保存在應用的臨時數組中,再通過List的subList方法,獲取到滿足條件的所有記錄。
實現:
首先在dao層,創建StudentMapper接口,用於對數據庫的操作。在接口中定義通過數組分頁的查詢方法,如下所示:
1 List<Student> queryStudentsByArray();
方法很簡單,就是獲取所有的數據,通過list接收后進行分頁操作。
創建StudentMapper.xml文件,編寫查詢的sql語句:
1 <select id="queryStudentsByArray" resultMap="studentmapper"> 2 select * from student 3 </select>
可以看出再編寫sql語句的時候,我們並沒有作任何分頁的相關操作。這里是查詢到所有的學生信息。
接下來在service層獲取數據並且進行分頁實現:
定義IStuService接口,並且定義分頁方法:
List<Student> queryStudentsByArray(int currPage, int pageSize);
通過接收currPage參數表示顯示第幾頁的數據,pageSize表示每頁顯示的數據條數。
創建IStuService接口實現類StuServiceIml對方法進行實現,對獲取到的數組通過currPage和pageSize進行分頁:
1 @Override 2 public List<Student> queryStudentsByArray(int currPage, int pageSize) { 3 List<Student> students = studentMapper.queryStudentsByArray(); 4 // 從第幾條數據開始 5 int firstIndex = (currPage - 1) * pageSize; 6 // 到第幾條數據結束 7 int lastIndex = currPage * pageSize; 8 return students.subList(firstIndex, lastIndex); 9 }
通過subList方法,獲取到兩個索引間的所有數據。
缺點:數據庫查詢並返回所有的數據,而我們需要的只是極少數符合要求的數據。當數據量少時,還可以接受。當數據庫數據量過大時,每次查詢對數據庫和程序的性能都會產生極大的影響。
二.借助Sql語句進行分頁
在了解到通過數組分頁的缺陷后,我們發現不能每次都對數據庫中的所有數據都檢索。然后在程序中對獲取到的大量數據進行二次操作,這樣對空間和性能都是極大的損耗。所以我們希望能直接在數據庫語言中只檢索符合條件的記錄,不需要在通過程序對其作處理。這時,Sql語句分頁技術橫空出世。
實現:通過sql語句實現分頁也是非常簡單的,只是需要改變我們查詢的語句就能實現了,即在sql語句后面添加limit分頁語句。
首先還是在StudentMapper接口中添加sql語句查詢的方法,如下:
1 List<Student> queryStudentsBySql(Map<String,Object> data);
然后在StudentMapper.xml文件中編寫sql語句通過limiy關鍵字進行分頁:
1 <select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper"> 2 select * from student limit #{currIndex} , #{pageSize} 3 </select>
接下來還是在IStuService接口中定義方法,並且在StuServiceIml中對sql分頁實現。
sql分頁語句如下:
1 select * from table limit index, pageSize;
所以在service中計算出currIndex:要開始查詢的第一條記錄的索引。
結果:
從輸出結果可以看出和數組分頁的結果是一致的,因此sql語句的分頁也是沒問題的。
缺點:雖然這里實現了按需查找,每次檢索得到的是指定的數據。但是每次在分頁的時候都需要去編寫limit語句,很冗余。而且不方便統一管理,維護性較差。所以我們希望能夠有一種更方便的分頁實現。
三.攔截器分頁
上面提到的數組分頁和sql語句分頁都不是我們今天講解的重點,今天需要實現的是利用攔截器達到分頁的效果。自定義攔截器實現了攔截所有以ByPage結尾的查詢語句,並且利用獲取到的分頁相關參數統一在sql語句后面加上limit分頁的相關語句,一勞永逸。不再需要在每個語句中單獨去配置分頁相關的參數了。
首先我們看一下攔截器的具體實現,在這里我們需要攔截所有以ByPage結尾的所有查詢語句,因此要使用該攔截器實現分頁功能,那么再定義名稱的時候需要滿足它攔截的規則(以ByPage結尾),如下所示:
1 package com.cbg.interceptor; 2 import org.apache.ibatis.executor.Executor; 3 import org.apache.ibatis.executor.parameter.ParameterHandler; 4 import org.apache.ibatis.executor.resultset.ResultSetHandler; 5 import org.apache.ibatis.executor.statement.StatementHandler; 6 import org.apache.ibatis.mapping.MappedStatement; 7 import org.apache.ibatis.plugin.*; 8 import org.apache.ibatis.reflection.MetaObject; 9 import org.apache.ibatis.reflection.SystemMetaObject; 10 import java.sql.Connection; 11 import java.util.Map; 12 import java.util.Properties; 13 14 /** 15 * @Intercepts 說明是一個攔截器 16 * @Signature 攔截器的簽名 17 * type 攔截的類型 四大對象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler) 18 * method 攔截的方法 19 * args 參數 20 */ 21 @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) 22 public class MyPageInterceptor implements Interceptor { 23 24 //每頁顯示的條目數 25 private int pageSize; 26 //當前現實的頁數 27 private int currPage; 28 29 private String dbType; 30 31 @Override 32 public Object intercept(Invocation invocation) throws Throwable { 33 //獲取StatementHandler,默認是RoutingStatementHandler 34 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); 35 //獲取statementHandler包裝類 36 MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler); 37 //分離代理對象鏈 38 while (MetaObjectHandler.hasGetter("h")) { 39 Object obj = MetaObjectHandler.getValue("h"); 40 MetaObjectHandler = SystemMetaObject.forObject(obj); 41 } 42 while (MetaObjectHandler.hasGetter("target")) { 43 Object obj = MetaObjectHandler.getValue("target"); 44 MetaObjectHandler = SystemMetaObject.forObject(obj); 45 } 46 //獲取連接對象 47 //Connection connection = (Connection) invocation.getArgs()[0]; 48 //object.getValue("delegate"); 獲取StatementHandler的實現類 49 //獲取查詢接口映射的相關信息 50 MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement"); 51 String mapId = mappedStatement.getId(); 52 //statementHandler.getBoundSql().getParameterObject(); 53 //攔截以.ByPage結尾的請求,分頁功能的統一實現 54 if (mapId.matches(".+ByPage$")) { 55 //獲取進行數據庫操作時管理參數的handler 56 ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler"); 57 //獲取請求時的參數 58 Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject(); 59 //也可以這樣獲取 60 //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject(); 61 //參數名稱和在service中設置到map中的名稱一致 62 currPage = (int) paraObject.get("currPage"); 63 pageSize = (int) paraObject.get("pageSize"); 64 String sql = (String) MetaObjectHandler.getValue("delegate.boundSql.sql"); 65 //也可以通過statementHandler直接獲取 66 //sql = statementHandler.getBoundSql().getSql(); 67 //構建分頁功能的sql語句 68 String limitSql; 69 sql = sql.trim(); 70 limitSql = sql + " limit " + (currPage - 1) * pageSize + "," + pageSize; 71 //將構建完成的分頁sql語句賦值個體'delegate.boundSql.sql',偷天換日 72 MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql); 73 } 74 //調用原對象的方法,進入責任鏈的下一級 75 return invocation.proceed(); 76 } 77 78 //獲取代理對象 79 @Override 80 public Object plugin(Object o) { 81 //生成object對象的動態代理對象 82 return Plugin.wrap(o, this); 83 } 84 85 //設置代理對象的參數 86 @Override 87 public void setProperties(Properties properties) { 88 //如果項目中分頁的pageSize是統一的,也可以在這里統一配置和獲取,這樣就不用每次請求都傳遞pageSize參數了。參數是在配置攔截器時配置的。 89 String limit1 = properties.getProperty("limit", "10"); 90 this.pageSize = Integer.valueOf(limit1); 91 this.dbType = properties.getProperty("dbType", "mysql"); 92 } 93 }
上面即是攔截器功能的實現,在intercept方法中獲取到select標簽和sql語句的相關信息,攔截所有以ByPage結尾的select查詢,並且統一在查詢語句后面添加limit分頁的相關語句,統一實現分頁功能。
重點詳解:
StatementHandler是一個接口,而我們在代碼中通過StatementHandler statementHandler = (StatementHandler) invocation.getTarget();獲取到的是StatementHandler默認的實現類RoutingStatementHandler。而RoutingStatementHandler只是一個中間代理,他不會提供具體的方法。那你可能會納悶了,攔截器中基本上是依賴statementHandler獲取各種對象和屬性的,沒有具體屬性和方法怎么行??接着看下面代碼:
1 private final StatementHandler delegate; 2 public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { 3 switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) { 4 case 1: 5 this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); 6 break; 7 case 2: 8 this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); 9 break; 10 case 3: 11 this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); 12 break; 13 default: 14 throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); 15 } 16 }
原來它是通過不同的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,還可以拿到下面的數據:
1 public abstract class BaseStatementHandler implements StatementHandler { 2 protected final Configuration configuration; 3 protected final ObjectFactory objectFactory; 4 protected final TypeHandlerRegistry typeHandlerRegistry; 5 protected final ResultSetHandler resultSetHandler; 6 protected final ParameterHandler parameterHandler; 7 protected final Executor executor; 8 protected final MappedStatement mappedStatement; 9 protected final RowBounds rowBounds; 10 protected BoundSql boundSql; 11 12 protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { 13 this.configuration = mappedStatement.getConfiguration(); 14 this.executor = executor; 15 this.mappedStatement = mappedStatement; 16 this.rowBounds = rowBounds; 17 this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); 18 this.objectFactory = this.configuration.getObjectFactory(); 19 if(boundSql == null) { 20 this.generateKeys(parameterObject); 21 boundSql = mappedStatement.getBoundSql(parameterObject); 22 } 23 this.boundSql = boundSql; 24 this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); 25 this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql); 26 }
上面的所有數據都可以通過反射拿到。
幾個重要的參數:
Configuration:所有配置的相關信息。
ResultSetHandler:用於攔截執行結果的組裝。
ParameterHandler:攔截執行Sql的參數的組裝。
Executor:執行Sql的全過程,包括組裝參數、組裝結果和執行Sql的過程。
BoundSql:執行的Sql的相關信息。
接下來我們通過如下代碼拿到請求時的map對象(反射)。
//獲取進行數據庫操作時管理參數的handler
1 ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler"); 2 //獲取請求時的參數 3 Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject(); 4 //也可以這樣獲取 5 //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();
拿到我們需要的currPage和pageSize參數后,就是組裝分頁查詢的sql語句’limitSql‘了。
最后通過MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);將原始的sql語句替換成我們新的分頁語句,完成偷天換日的功能,接下來讓代碼繼續執行。
編寫好攔截器后,需要注冊到項目中,才能發揮它的作用。在mybatis的配置文件中,添加如下代碼:
1 <plugins> 2 <plugin interceptor="com.cbg.interceptor.MyPageInterceptor"> 3 <property name="limit" value="10"/> 4 <property name="dbType" value="mysql"/> 5 </plugin> 6 </plugins>
如上所示,還能在里面配置一些屬性,在攔截器的setProperties方法中可以獲取配置好的屬性值。如項目分頁的pageSize參數的值固定,我們就可以配置在這里了,以后就不需要每次傳入pageSize了,讀取方式如下:
//讀取配置的代理對象的參數
1 @Override 2 public void setProperties(Properties properties) { 3 String limit1 = properties.getProperty("limit", "10"); 4 this.pageSize = Integer.valueOf(limit1); 5 this.dbType = properties.getProperty("dbType", "mysql"); 6 }
到這里,有關攔截器的相關知識就講解的差不多了,接下來就需要測試,是否我們這樣寫真的有效??
首先還是添加dao層的方法和xml文件的sql語句配置,注意項目中攔截的是以ByPage結尾的請求,所以在這里,我們的方法名稱也以此結尾:
方法
1 List<Student> queryStudentsByPage(Map<String,Object> data);
xml文件的select語句
1 <select id="queryStudentsByPage" parameterType="map" resultMap="studentmapper"> 2 select * from student 3 </select>
可以看出,這里我們就不需要再去手動配置分頁語句了。
接下來是service層的接口編寫和實現方法:
方法:
1 List<Student> queryStudentsByPage(int currPage,int pageSize);
實現:
1 @Override 2 public List<Student> queryStudentsByPage(int currPage, int pageSize) { 3 Map<String, Object> data = new HashedMap(); 4 data.put("currPage", currPage); 5 data.put("pageSize", pageSize); 6 return studentMapper.queryStudentsByPage(data); 7 }
這里我們雖然傳入了currPage和pageSize兩個參數,但是在sql的xml文件中並沒有使用,直接在攔截器中獲取到統一使用。
最后編寫controller的測試代碼:
1 @ResponseBody 2 @RequestMapping("/student/page/{currPage}/{pageSize}") 3 public List<Student> getStudentByPage(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) { 4 List<Student> student = StuServiceIml.queryStudentsByPage(currPage, pageSize); 5 return student; 6 }
結果:
可見和上面兩種分頁的效果是一樣的。
四.RowBounds實現分頁
原理:通過RowBounds實現分頁和通過數組方式分頁原理差不多,都是一次獲取所有符合條件的數據,然后在內存中對大數據進行操作,實現分頁效果。只是數組分頁需要我們自己去實現分頁邏輯,這里更加簡化而已。
存在問題:一次性從數據庫獲取的數據可能會很多,對內存的消耗很大,可能導師性能變差,甚至引發內存溢出。
適用場景:在數據量很大的情況下,建議還是適用攔截器實現分頁效果。RowBounds建議在數據量相對較小的情況下使用。
簡單介紹:這是代碼實現上最簡單的一種分頁方式,只需要在dao層接口中要實現分頁的方法中加入RowBounds參數,然后在service層通過offset(從第幾行開始讀取數據,默認值為0)和limit(要顯示的記錄條數,默認為java允許的最大整數:2147483647)兩個參數構建出RowBounds對象,在調用dao層方法的時,將構造好的RowBounds傳進去就能輕松實現分頁效果了。
具體操作如下:
dao層接口方法:
1 //加入RowBounds參數 2 public List<UserBean> queryUsersByPage(String userName, RowBounds rowBounds);
然后在service層構建RowBounds,調用dao層方法:
1 @Override 2 @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.SUPPORTS) 3 public List<RoleBean> queryRolesByPage(String roleName, int start, int limit) { 4 return roleDao.queryRolesByPage(roleName, new RowBounds(start, limit)); 5 }
RowBounds就是一個封裝了offset和limit簡單類,如下所示:
1 public class RowBounds { 2 public static final int NO_ROW_OFFSET = 0; 3 public static final int NO_ROW_LIMIT = 2147483647; 4 public static final RowBounds DEFAULT = new RowBounds(); 5 private int offset; 6 private int limit; 7 8 public RowBounds() { 9 this.offset = 0; 10 this.limit = 2147483647; 11 } 12 13 public RowBounds(int offset, int limit) { 14 this.offset = offset; 15 this.limit = limit; 16 } 17 18 public int getOffset() { 19 return this.offset; 20 } 21 22 public int getLimit() { 23 return this.limit; 24 }
結論:從上面四種sql分頁的實現方式可以看出,通過RowBounds實現是最簡便的,但是通過攔截器的實現方式是最優的方案。只需一次編寫,所有的分頁方法共同使用,還可以避免多次配置時的出錯機率,需要修改時也只需要修改這一個文件,一勞永逸。而且是我們自己實現的,便於我們去控制和增加一些邏輯處理,使我們在外層更簡單的使用。同時也不會出現數組分頁和RowBounds分頁導致的性能問題。當然,具體情況可以采取不同的解決方案。數據量小時,RowBounds不失為一種好辦法。但是數據量大時,實現攔截器就很有必要了。
本文轉載自:https://www.cnblogs.com/guanghe/p/10056893.html