【深入淺出MyBatis系列七】分頁外掛
陶邦仁 釋出於 2015/12/24 15:39
系列目錄
- 深入淺出MyBatis系列
- 【深入淺出MyBatis系列一】MyBatis入門
- 【深入淺出MyBatis系列二】配置簡介(MyBatis原始碼篇)
- 【深入淺出MyBatis系列三】Mapper對映檔案配置
- 【深入淺出MyBatis系列四】強大的動態SQL
- 【深入淺出MyBatis系列五】SQL執行流程分析(原始碼篇)
- 【深入淺出MyBatis系列六】外掛原理
- 【深入淺出MyBatis系列七】分頁外掛
- 【深入淺出MyBatis系列八】SQL自動生成外掛
- 【深入淺出MyBatis系列九】改造Cache外掛
- 【深入淺出MyBatis系列十】與Spring整合
- 【深入淺出MyBatis系列十一】快取原始碼分析
- 【深入淺出MyBatis系列十二】終結篇:MyBatis原理深入解析
Mybatis的分頁功能很弱,它是基於記憶體的分頁(查出所有記錄再按偏移量和limit取結果
),在大資料量的情況下這樣的分頁基本上是沒有用的
。本文基於外掛,通過攔截StatementHandler重寫sql語句,實現資料庫的物理分頁
。
1 準備
1.1 為什麼在StatementHandler攔截## 在SQL執行流程分析(原始碼篇)章節介紹了一次sqlsession的完整執行過程,從中可以知道sql的解析是在StatementHandler裡完成的,所以為了重寫sql需要攔截StatementHandler。
1.2 MetaObject簡介
在實現裡大量使用了MetaObject這個物件,因此有必要先介紹下它。MetaObject是Mybatis提供的一個的工具類,通過它包裝一個物件後可以獲取或設定該物件的原本不可訪問的屬性(比如那些私有屬性)
。它有個三個重要方法經常用到:
MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) 用於包裝物件;
Object getValue(String name) 用於獲取屬性的值(支援OGNL的方法);
void setValue(String name, Object value) 用於設定屬性的值(支援OGNL的方法);
2 攔截器簽名
@Intercepts({@Signature(type =StatementHandler.class, method = "prepare", args ={Connection.class})})
public class PageInterceptor implements Interceptor {
...
}
從簽名裡可以看出,要攔截的目標型別是StatementHandler(注意:type只能配置成介面型別)
,攔截的方法是名稱為prepare引數為Connection型別的方法。
3 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指示攔截的規則,以正則方式匹配
。
4 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;
}
5 分頁引數重寫
有時候會有這種需求,就是不但要查出指定頁的結果,還需要知道總的記錄數和頁數。我通過重寫分頁引數的方式提供了一種解決方案:
/**
* 從資料庫裡查詢總的記錄數並計算總頁數,回寫進分頁引數<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);
}
6 plugin實現
public Object plugin(Object target) {
// 當目標類是StatementHandler型別時,才包裝目標類,否者直接返回目標本身,減少目標被代理的
// 次數
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
© 著作權歸作者所有
相關文章
- mybatis generator外掛系列--分頁外掛MyBatis
- 深入淺出MyBatis:MyBatis外掛及開發過程MyBatis
- myBatis分頁外掛配置MyBatis
- mybatis plus 新增分頁外掛MyBatis
- 深入淺出Service外掛化原理
- 深入淺出Mybatis原始碼系列(一)---Mybatis入門MyBatis原始碼
- mybatis的三發外掛:分頁pagehelpMyBatis
- 使用mybatis分頁外掛展示首頁最新視訊MyBatis
- 手把手教你開發 MyBatis 分頁外掛MyBatis
- Mybatis分頁外掛只顯示第一頁的問題MyBatis
- 深入淺出MyBatis:MyBatis的所有配置MyBatis
- Mybatis第三方PageHelper分頁外掛原理MyBatis
- VSCode For Web 深入淺出 -- 外掛載入機制VSCodeWeb
- 深入淺出MyBatis:JDBC和MyBatis介紹MyBatisJDBC
- 深入淺出的webpack構建工具---AutoDllPlugin外掛(八)WebPlugin
- 深入淺出Tomcat系列Tomcat
- 深入淺出MyBatis:MyBatis解析和執行原理MyBatis
- 淺析MyBatis(三):聊一聊MyBatis的實用外掛與自定義外掛MyBatis
- spring boot(二)整合mybatis plus+ 分頁外掛 + 程式碼生成Spring BootMyBatis
- SpringBoot中使用Mybatis-plus整合PageHelper分頁外掛踩坑Spring BootMyBatis
- mybatis generator外掛系列--lombok外掛 (減少百分之九十bean程式碼)MyBatisLombokBean
- Springboot+Mybatis+Mybatisplus 框架中增加自定義分頁外掛和sql 佔位符修改外掛Spring BootMyBatis框架SQL
- 深入淺出Zookeeper(七):Leader選舉
- mybatisPlus分頁外掛的使用MyBatis
- MyBatis外掛MyBatis
- 深入淺出MyBatis:反射和動態代理MyBatis反射
- 《Apache RocketMQ 深入淺出》系列文章ApacheMQ
- 深入淺出MyBatis:MyBatis與Spring整合及實用場景MyBatisSpring
- jquery寫的ajax分頁外掛jQuery
- 深入淺出MyBatis:「對映器」全瞭解MyBatis
- mybatis plus 啟用 mybatis外掛MyBatis
- MyBatis 的外掛物件如何建立出來的MyBatis物件
- MybatisPlus的分頁外掛簡單使用MyBatis
- [分散式]Nginx系列文章---深入淺出Nginx分散式Nginx
- 深入淺出etcd系列 – 心跳和選舉
- MyBatis外掛 - 通用mapperMyBatisAPP
- mybatis 自定義外掛MyBatis
- Mybatis外掛開發MyBatis