Mybatis的分頁功能很弱,它是基於內存的分頁(查出全部記錄再按偏移量和limit取結果),在大數據量的狀況下這樣的分頁基本上是沒有用的。本文基於插件,經過攔截StatementHandler重寫sql語句,實現數據庫的物理分頁。本文適配的mybatis版本是3.2.2。html
準備
爲何在StatementHandler攔截
在深刻淺出MyBatis-Sqlsession章節介紹了一次sqlsession的完整執行過程,從中能夠知道sql的解析是在StatementHandler裏完成的,因此爲了重寫sql須要攔截StatementHandler。java
MetaObject簡介
在個人實現裏大量使用了MetaObject這個對象,所以有必要先介紹下它。MetaObject是Mybatis提供的一個的工具類,經過它包裝一個對象後能夠獲取或設置該對象的本來不可訪問的屬性(好比那些私有屬性)。它有個三個重要方法常常用到:mysql
1) MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)sql
2) Object getValue(String name)數據庫
3) void setValue(String name, Object value)session
方法1)用於包裝對象;方法2)用於獲取屬性的值(支持OGNL的方法);方法3)用於設置屬性的值(支持OGNL的方法);mybatis
插件的原理
參見深刻淺出Mybatis-插件原理。oracle
有了上面這些基礎知識的準備後,就能夠咱們的主題了。app
攔截器簽名
@Intercepts({@Signature(type =StatementHandler.class, method = "prepare", args ={Connection.class})}) publicclass PageInterceptor implementsInterceptor { ... }
從簽名裏能夠看出,要攔截的目標類型是StatementHandler(注意:type只能配置成接口類型),攔截的方法是名稱爲prepare參數爲Connection類型的方法。工具
intercept的實現
public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); // 分離代理對象鏈(因爲目標類可能被多個攔截器攔截,從而造成屢次代理,經過下面的兩次循環 // 能夠分離出最原始的的目標類) while (metaStatementHandler.hasGetter("h")) { Object object = metaStatementHandler.getValue("h"); metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); } // 分離最後一個代理對象的目標類 while (metaStatementHandler.hasGetter("target")) { Object object = metaStatementHandler.getValue("target"); metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); } Configuration configuration = (Configuration) metaStatementHandler. getValue("delegate.configuration"); dialect = configuration.getVariables().getProperty("dialect"); if (null == dialect || "".equals(dialect)) { logger.warn("Property dialect is not setted,use default 'mysql' "); dialect = defaultDialect; } pageSqlId = configuration.getVariables().getProperty("pageSqlId"); if (null == pageSqlId || "".equals(pageSqlId)) { logger.warn("Property pageSqlId is not setted,use default '.*Page$' "); pageSqlId = defaultPageSqlId; } MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement"); // 只重寫須要分頁的sql語句。經過MappedStatement的ID匹配,默認重寫以Page結尾的 // MappedStatement的sql if (mappedStatement.getId().matches(pageSqlId)) { BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql"); Object parameterObject = boundSql.getParameterObject(); if (parameterObject == null) { throw new NullPointerException("parameterObject is null!"); } else { // 分頁參數做爲參數對象parameterObject的一個屬性 PageParameter page = (PageParameter) metaStatementHandler .getValue("delegate.boundSql.parameterObject.page"); String sql = boundSql.getSql(); // 重寫sql String pageSql = buildPageSql(sql, page); metaStatementHandler.setValue("delegate.boundSql.sql", pageSql); // 採用物理分頁後,就不須要mybatis的內存分頁了,因此重置下面的兩個參數 metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET); metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT); Connection connection = (Connection) invocation.getArgs()[0]; // 重設分頁參數裏的總頁數等 setPageParameter(sql, connection, mappedStatement, boundSql, page); } } // 將執行權交給下一個攔截器 return invocation.proceed(); }
StatementHandler的默認實現類是RoutingStatementHandler,所以攔截的實際對象是它。RoutingStatementHandler的主要功能是分發,它根據配置Statement類型建立真正執行數據庫操做的StatementHandler,並將其保存到delegate屬性裏。因爲delegate是一個私有屬性而且沒有提供訪問它的方法,所以須要藉助MetaObject的幫忙。經過MetaObject的封裝後咱們能夠輕易的得到想要的屬性。
在上面的方法裏有個兩個循環,經過他們能夠分離出原始的RoutingStatementHandler(而不是代理對象)。
前面提到,簽名裏配置的要攔截的目標類型是StatementHandler攔截的方法是名稱爲prepare參數爲Connection類型的方法,而這個方法是每次數據庫訪問都要執行的。由於我是經過重寫sql的方式實現分頁,爲了避免影響其餘sql(update或不須要分頁的query),我採用了經過ID匹配的方式過濾。默認的過濾方式只對id以Page結尾的進行攔截(注意區分大小寫),以下:
<select id="queryUserByPage" parameterType="UserDto" resultType="UserDto"> <![CDATA[ select * from t_user t where t.username = #{username} ]]> </select>
固然,也能夠自定義攔截模式,在mybatis的配置文件里加入如下配置項:
<properties> <property name="dialect" value="mysql" /> <property name="pageSqlId" value=".*Page$" /> </properties>
其中,屬性dialect指示數據庫類型,目前只支持mysql和oracle兩種數據庫。其中,屬性pageSqlId指示攔截的規則,以正則方式匹配。
sql重寫
sql重寫其實在原始的sql語句上加入分頁的參數,目前支持mysql和oracle兩種數據庫的分頁。
private String buildPageSql(String sql, PageParameter page) { if (page != null) { StringBuilder pageSql = new StringBuilder(); if ("mysql".equals(dialect)) { pageSql = buildPageSqlForMysql(sql, page); } else if ("oracle".equals(dialect)) { pageSql = buildPageSqlForOracle(sql, page); } else { return sql; } return pageSql.toString(); } else { return sql; } }
mysql的分頁實現:
public StringBuilder buildPageSqlForMysql(String sql, PageParameter page) { StringBuilder pageSql = new StringBuilder(100); String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize()); pageSql.append(sql); pageSql.append(" limit " + beginrow + "," + page.getPageSize()); return pageSql; }
oracle的分頁實現:
public StringBuilder buildPageSqlForOracle(String sql, PageParameter page) { StringBuilder pageSql = new StringBuilder(100); String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize()); String endrow = String.valueOf(page.getCurrentPage() * page.getPageSize()); pageSql.append("select * from ( select temp.*, rownum row_id from ( "); pageSql.append(sql); pageSql.append(" ) temp where rownum <= ").append(endrow); pageSql.append(") where row_id > ").append(beginrow); return pageSql; }
分頁參數重寫
有時候會有這種需求,就是不但要查出指定頁的結果,還須要知道總的記錄數和頁數。我經過重寫分頁參數的方式提供了一種解決方案:
/** * 從數據庫裏查詢總的記錄數並計算總頁數,回寫進分頁參數<code>PageParameter</code>,這樣調用 * 者就可用經過 分頁參數<code>PageParameter</code>得到相關信息。 * * @param sql * @param connection * @param mappedStatement * @param boundSql * @param page */ private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement, BoundSql boundSql, PageParameter page) { // 記錄總記錄數 String countSql = "select count(0) from (" + sql + ") as total"; PreparedStatement countStmt = null; ResultSet rs = null; try { countStmt = connection.prepareStatement(countSql); BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), boundSql.getParameterObject()); setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject()); rs = countStmt.executeQuery(); int totalCount = 0; if (rs.next()) { totalCount = rs.getInt(1); } page.setTotalCount(totalCount); int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1); page.setTotalPage(totalPage); } catch (SQLException e) { logger.error("Ignore this exception", e); } finally { try { rs.close(); } catch (SQLException e) { logger.error("Ignore this exception", e); } try { countStmt.close(); } catch (SQLException e) { logger.error("Ignore this exception", e); } } } /** * 對SQL參數(?)設值 * * @param ps * @param mappedStatement * @param boundSql * @param parameterObject * @throws SQLException */ private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException { ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql); parameterHandler.setParameters(ps); }
plugin的實現
public Object plugin(Object target) { // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標自己,減小目標被代理的 // 次數 if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } else { return target; } }
源碼
下載地址:點擊打開連接