MyBatis:簡單物理分頁實現

來源:互聯網
上載者:User

標籤:

一、必要性

首先,介紹一下使用自訂攔截器來進行物理分頁的必要性。我們知道MyBatis中的SqlSession介面中提供一個帶分頁功能的方法:

public interface SqlSession extends Closeable {    <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);    // ....}

使用該方法,我們在查詢時可以通過為selectList(..)方法提供一個RowBounds參數,來使該語句帶有分頁功能,舉個例如,假設我需要取出查詢記錄的前三條記錄,可以這樣:

// 擷取sqlSession的步驟略,statement略,mapper中的映射語句為 select * from usersList<User> list = sqlSession.selectList(statement, null, new RowBounds(0,3));

這時我們擷取到的記錄就是查詢記錄的前三條記錄(select * from users的查詢結果)

這時我們會有個疑問,既然MyBatis已經為我們提供了分頁的處理類,為何我們還要再重複造輪子(再手動寫一個攔截器)呢?

這是因為MyBatis內建的分頁處理器,是通過記憶體進行分頁,結合上面的例子就是MyBatis首先執行select * from users,然後擷取結果集ResultSet,接著通過傳入的RowBounds中的offset和limit屬性來對ResultSet進行加工。如果記錄量大的話,這種效率無疑是相當低的。想證實上面這個結論,可以查看MyBatis中的DefaultResultSetHandler類

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方式執行

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資料庫),這樣效率顯然是極低的,所以我們如果在實際應用中,有兩種方式來解決這個問題

1)在mapper對應檔中手動將每次執行的語句改為select * from users limit #{offset},#{limit}

2)自訂一個攔截器,來將底層的最終查詢語句變更為select * from users limit 0,3

下面將介紹第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語句Regex匹配: ^代表開頭位置 \s代表空格 $代表結尾位置 *代表任意多個 .代表任一字元private final static String REGEX = "^\\s*[Ss][Ee][Ll][Ee][Cc][Tt].*$";@Overridepublic 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來指定具體的StatementHandlerObject delegate = readField(target, "delegate");// rowBounds中綁定了我們自訂的分頁資訊,包括起始位置offset和取出記錄條數limitRowBounds 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);}@Overridepublic Object plugin(Object target) {// 通過Plugin的wrap(...)方法來實現代理類的產生操作return Plugin.wrap(target, this);}@Overridepublic 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 {@Testpublic 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);} }


MyBatis:簡單物理分頁實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.