1、必要性
java
首先,介紹一下使用自定義攔截器來進行物理分頁的必要性。咱們知道MyBatis中的SqlSession接口中提供一個帶分頁功能的方法:正則表達式
public interface SqlSession extends Closeable { <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds); // .... }
使用該方法,咱們在查詢時能夠經過爲selectList(..)方法提供一個RowBounds參數,來使該語句帶有分頁功能,舉個例如,假設我須要取出查詢記錄的前三條記錄,能夠這樣:sql
// 獲取sqlSession的步驟略,statement略,mapper中的映射語句爲 select * from users List<User> list = sqlSession.selectList(statement, null, new RowBounds(0,3));
這時咱們獲取到的記錄就是查詢記錄的前三條記錄(select * from users的查詢結果)數據庫
這時咱們會有個疑問,既然MyBatis已經爲咱們提供了分頁的處理類,爲什麼咱們還要再重複造輪子(再手動寫一個攔截器)呢?apache
這是由於MyBatis內置的分頁處理器,是經過內存進行分頁,結合上面的例子就是MyBatis首先執行select * from users,而後獲取結果集ResultSet,接着經過傳入的RowBounds中的offset和limit屬性來對ResultSet進行加工。若是記錄量大的話,這種效率無疑是至關低的。想證明上面這個結論,能夠查看MyBatis中的DefaultResultSetHandler類session
public class DefaultResultSetHandler implements ResultSetHandler { // .... private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>(); // 經過skipRows來使ResusltSet指向rowBounds中的offset所指定的位置 skipRows(rsw.getResultSet(), rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null); Object rowValue = getRowValue(rsw, discriminatedResultMap); storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet()); } } private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException { // 若是ResultSet中的光標支持先後移動 if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) { // 若是rowBounds中的offset值不爲0 if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) { // 將rs的光標移至rowBounds中的offset指定的位置 rs.absolute(rowBounds.getOffset()); } } else { // 若是ResultSet只支持向前移動 // 將光標從0的位置使用rs.next()來每次向前移動一位,直至rowBounds中offset指定的位置 for (int i = 0; i < rowBounds.getOffset(); i++) { rs.next(); } } } // .... }
欲證明可在這個類中的handleRowValuesForSimpleResultMap(...)中的skipRows(...)方法上打個斷點,在skipRows(...)中的rs.absolute(...)和rs.next()分別打上斷點,而後經過debug方式執行mybatis
List<User> list = sqlSession.selectList(statement, null, new RowBounds(0,3));
便可看到程序確實進入了skipRows(...)方法中,咱們能夠看到在skipRows(...)中,MyBatis是經過執行select * from users再對ResultSet結果集進行加工處理,而不是直接執行select * from users limit 0,3(假設是MySql數據庫),這樣效率顯然是極低的,因此咱們若是在實際應用中,有兩種方式來解決這個問題app
1)在mapper映射文件中手動將每次執行的語句改成select * from users limit #{offset},#{limit}dom
2)自定義一個攔截器,來將底層的最終查詢語句變動爲select * from users limit 0,3ide
下面將介紹第2種解決方式。
2、自定義分頁插件
MyBatis 容許你在已映射語句執行過程當中的某一點進行攔截調用。默認狀況下,MyBatis 容許使用插件來攔截的方法調用包括:
// 前面是容許用插件攔截的類名,括號裏是容許用插件攔截的方法名 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query)
MyBatis是在StatementHandler中的prepare(...)方法中完成對sql的解析,因此咱們須要在這個方法前設置一個攔截器也就是plugin來進行sql語句的置換,下面是具體的代碼:
package cn.kolbe.mybatis.plugin; import java.lang.reflect.Field; import java.sql.Connection; import java.util.Properties; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.RowBounds; @Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class })) public class PagePlugin implements Interceptor { // select語句正則表達式匹配: ^表明開頭位置 \s表明空格 $表明結尾位置 *表明任意多個 .表明任意字符 private final static String REGEX = "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt].*$"; @Override public Object intercept(Invocation inv) throws Throwable { // 此時的target爲RoutingStatementHandler類的實例 StatementHandler target = (StatementHandler)inv.getTarget(); // BoundSql類中有一個sql屬性,即爲待執行的sql語句 BoundSql boundSql = target.getBoundSql(); String sql = boundSql.getSql(); // 若是sql語句是select語句的話,則進行查看是否須要分頁處理 if (sql.matches(REGEX)) { // delegate是RoutingStatementHandler經過mapper映射文件中設置的statementType來指定具體的StatementHandler Object delegate = readField(target, "delegate"); // rowBounds中綁定了咱們自定義的分頁信息,包括起始位置offset和取出記錄條數limit RowBounds rowBounds = (RowBounds)readField(delegate, "rowBounds"); // 若是rowBound不爲空,且rowBounds的起始位置不爲0,則表明咱們須要進行分頁處理 if (rowBounds != null && rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) { // assemSql(...)完成對sql語句的裝配及rowBounds的重置操做 writeField(boundSql, "sql", assemSql(sql, rowBounds)); } } return inv.proceed(); } /** * 裝配SQL語句,並重置RowBounds中的offset和limit * @param oldSql * @param rowBounds * @return */ public String assemSql(String oldSql, RowBounds rowBounds) throws Exception { String sql = oldSql + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit(); // 這兩步是必須的,由於在前面置換好sql語句之後,實際的結果集就是咱們想要的因此offset和limit必須重置爲初始值 writeField(rowBounds, "offset", RowBounds.NO_ROW_OFFSET); writeField(rowBounds, "limit", RowBounds.NO_ROW_LIMIT); return sql; } /** * 利用反射獲取指定對象的指定屬性 * @param target * @param fieldName * @return * @throws Exception */ private Object readField(Object target, String fieldName) throws Exception { Field field = null; // 遍歷target的屬性及其父類的屬性 for (Class<?> c = target.getClass(); c != null; c = c.getSuperclass()) { try { field = c.getDeclaredField(fieldName); } catch (NoSuchFieldException ex) { // 沒找到該屬性,則繼承查找父類的屬性,因此不處理該異常 } } field.setAccessible(true); return field.get(target); } /** * 利用反射爲指定對象的指定屬性寫入值 * @param target * @param fieldName * @param value * @throws Exception */ private void writeField(Object target, String fieldName, Object value) throws Exception { Field field = null; // 遍歷target的屬性及其父類的屬性 for (Class<?> c = target.getClass(); c != null; c = c.getSuperclass()) { try { field = c.getDeclaredField(fieldName); } catch (NoSuchFieldException ex) { // 沒找到該屬性,則繼承查找父類的屬性,因此不處理該異常 } } field.setAccessible(true); field.set(target, value); } @Override public Object plugin(Object target) { // 經過Plugin的wrap(...)方法來實現代理類的生成操做 return Plugin.wrap(target, this); } @Override public void setProperties(Properties props) {} }
注:
1)代碼中爲了儘可能保持簡單易懂,沒有使用過多的工具集,具體應用中對對象的私有屬性賦值獲取和賦值操做能夠經過MyBatis內置的類或apache的commons-lang工具來處理
2)該例子使用MySql做爲示例,沒有考慮其它數據庫,具體應用中能夠考慮經過配置文件中來設置數據庫,並動態的根據配置文件來決定sql語句的具體裝配,一樣爲了簡單性,在此就不舉例了
在MyBatis的配置文件mybatis-config.xml中配置該插件
<plugins> <plugin interceptor="cn.kolbe.mybatis.plugin.PagePlugin"></plugin> </plugins>
在映射文件中添加查詢語句
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/mybatis-3-mapper.dtd"> <mapper namespace="cn.kolbe.mybatis.domain.UserMapper"> <select id="getAll" resultType="User"> select * from users </select> </mapper>
在應用中使用
package cn.kolbe.mybatis; import java.io.FileInputStream; import java.io.InputStream; import java.util.List; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import cn.kolbe.mybatis.domain.User; public class MyBatisTest { @Test public void queryByPage() throws Exception { InputStream in = new FileInputStream("src/main/java/mybatis-config.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in); SqlSession session = factory.openSession(); String statement = "cn.kolbe.mybatis.domain.UserMapper.getAll"; List<User> list = session.selectList(statement, null, new RowBounds(0,3)); System.out.println(list); } }