本文介紹使用Mybatis攔截器,實現分頁;並且在dao層,直接返回自定義的分頁對象。
2017-09-29修改:添加Mybatis攔截器分頁對oracle、sqlServer的支持
最終dao層結果:
public interface ModelMapper { Page<Model> pageByConditions(RowBounds rowBounds, Model record); }
接下來一步一步來實現分頁。
一.創建Page對象:
public class Page<T> extends ArrayList<T> { private static final long serialVersionUID = 1L; private int pageNo = 1;// 頁碼,默認是第一頁 private int pageSize = 15;// 每頁顯示的記錄數,默認是15 private int totalRecord;// 總記錄數 private int totalPage;// 總頁數 private List<T> result; public Page() { } public Page(int pageNo, int pageSize, int totalRecord, List<T> results) { this.pageNo = pageNo; this.pageSize = pageSize; this.totalRecord = totalRecord; this.setResult(results); int totalPage = totalRecord % pageSize == 0 ? totalRecord / pageSize : totalRecord / pageSize + 1; this.setTotalPage(totalPage); } public int getPageNo() { return pageNo; } public void setPageNo(int pageNo) { this.pageNo = pageNo; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalRecord() { return totalRecord; } public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; // 在設置總頁數的時候計算出對應的總頁數,在下面的三目運算中加法擁有更高的優先級,所以最后可以不加括號。 int totalPage = totalRecord % pageSize == 0 ? totalRecord / pageSize : totalRecord / pageSize + 1; this.setTotalPage(totalPage); } public int getTotalPage() { return totalPage; } public void setTotalPage(int totalPage) { this.totalPage = totalPage; } public List<T> getResult() { return result; } public void setResult(List<T> result) { this.result = result; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Page [pageNo=").append(pageNo).append(", pageSize=").append(pageSize).append(", results=") .append(getResult()).append(", totalPage=").append(totalPage).append(", totalRecord=").append(totalRecord) .append("]"); return builder.toString(); } }
為什么Page需要繼承ArrayList接口,這個會在稍后的代碼中做解釋。
二.提供Dao以及mapper.xml
dao的寫法:
Page<Model> pageByConditions(RowBounds rowBounds, Model record);
mapper.xml:
<!-- 表名 --> <sql id="tableName" > model </sql> <!-- 數據表所有列名 --> <sql id="Base_Column_List" > id, name </sql> <!-- 查詢字段 --> <sql id="Base_Search_Param" > <if test="id != null" > and id = #{id,jdbcType=INTEGER} </if> <if test="name != null" > and name = #{name,jdbcType=VARCHAR} </if> </sql> <!-- 分頁查詢語句 --> <select id="pageByConditions" resultMap="BaseResultMap"> SELECT <include refid="Base_Column_List" /> FROM <include refid="tableName" /> WHERE 1=1 <include refid="Base_Search_Param" /> </select>
ok,以上都是mybatis的基本操作,就不做多余解釋。
三.創建攔截器:
攔截器原理以及執行順序,可參考:http://www.cnblogs.com/fangjian0423/p/mybatis-interceptor.html、http://blog.csdn.net/abcd898989/article/details/51261163
我們需要做的是創建一個攔截器(PageInterceptor)、一個執行者(PageExecutor)。
1.PageInteceptor:實現Inteceptor接口,將PageExecutor進行執行,攔截sql添加分頁sql(limit xx,xx)
2.PageExecutor:實現Executor接口,在查詢時,添加查詢總數並修改返回值類型。因為要做的是分頁,是查詢操作,所以里邊的非查詢方法都使用基本的實現,只修改兩個query方法。
PageInteceptor完整代碼:
import java.lang.reflect.InvocationTargetException; import java.sql.Connection; import java.util.Properties; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; 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.reflection.MetaObject; import org.apache.ibatis.reflection.factory.DefaultObjectFactory; import org.apache.ibatis.reflection.factory.ObjectFactory; import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory; import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import cn.com.common.page.PageExecutor; @Intercepts({ @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }), @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) }) public class PageInterceptor implements Interceptor { private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory(); private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory(); private String pattern = "^.*page.*$"; // 需要進行分頁操作的字符串正則表達式 /** 數據庫方言:目前只支持mysql、oracle、sqlServer;默認mysql */ private String dialect = "mysql"; public String getPattern() { return pattern; } public void setPattern(String pattern) { this.pattern = pattern; } public String getDialect() { return dialect; } public void setDialect(String dialect) { this.dialect = dialect; } @Override public Object intercept(Invocation invocation) throws Throwable { if (invocation.getTarget() instanceof StatementHandler) { return handleStatementHandler(invocation); } return invocation.proceed(); } /** * @param invocation * @return * @throws IllegalAccessException * @throws InvocationTargetException */ private Object handleStatementHandler(Invocation invocation) throws InvocationTargetException, IllegalAccessException { StatementHandler statementHandler = (StatementHandler) invocation .getTarget(); MetaObject metaStatementHandler = MetaObject.forObject( statementHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY); RowBounds rowBounds = (RowBounds) metaStatementHandler .getValue("delegate.rowBounds"); if (rowBounds == null || (rowBounds.getOffset() == RowBounds.NO_ROW_OFFSET && rowBounds .getLimit() == RowBounds.NO_ROW_LIMIT)) { return invocation.proceed(); } // 分離代理對象鏈(由於目標類可能被多個攔截器攔截,從而形成多次代理,通過下面的兩次循環可以分離出最原始的的目標類) 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); } // 將mybatis的內存分頁,調整為物理分頁 BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql"); String sql = boundSql.getSql(); // 重寫sql String pageSql = PageSqlFactory.getPageSqlByDialect(this.dialect, sql, rowBounds); 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); // 將執行權交給下一個攔截器 return invocation.proceed(); } @Override public Object plugin(Object o) { if (Executor.class.isAssignableFrom(o.getClass())) { PageExecutor executor = new PageExecutor((Executor)o, pattern); return Plugin.wrap(executor, this); } else if (o instanceof StatementHandler) { return Plugin.wrap(o, this); } return o; } @Override public void setProperties(Properties properties) { } }
PageSqlFactory在工廠中根據數據庫方言創建分頁sql
public class PageSqlFactory { /** * 根據數據庫方言獲取分頁查詢語句(目前只支持mysql、oracle、sqlServer;默認mysql) * @param dialect * @param originalSql * @param rowBounds * @return * @author Taocong * @date 2017年9月28日 下午4:12:57 */ public static String getPageSqlByDialect(String dialect, String originalSql, RowBounds rowBounds) { String pageSql = null; if (null == dialect || "mysql".equals(dialect)) { pageSql = originalSql + " LIMIT " + rowBounds.getOffset() + "," + rowBounds.getLimit(); } else if ("oracle".equals(dialect)) { StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("select * from ( select tmp_page.*, rownum row_id from ( "); sqlBuilder.append(originalSql); sqlBuilder.append(" ) tmp_page where rownum <= "); sqlBuilder.append(rowBounds.getLimit()); sqlBuilder.append(" ) where row_id > "); sqlBuilder.append(rowBounds.getOffset()); pageSql = sqlBuilder.toString(); } else if ("sqlServer".equals(dialect)) { /* 查看sql中是否有排序規則: * 1.如果沒有,按手動新增字段n(n=0)生成rown_number * 2.如果有,按指定的排序規則生成rown_number * * eg: * 1.select * from a where 1=1 * ->select * from (select row_number() over(ORDER BY n) as rownumber,* * from ( select top 20 n=0, * from a where 1=1)t )tt where rownumber> 0 * * 2.select * from a where 1=1 ORDER BY b * ->select * from (select row_number() over(ORDER BY b) as rownumber,* * from ( select top 20 n=0, * from a where 1=1 ORDER BY b)t )tt where rownumber> 0 */ // 找到sql中的排序 // (由於indexof不能使用正則,所以先使用replaceAll對sql中的排序語法規范化;(?i)標識忽略大小寫;\\s*表示空格出現一次或多次) int orderStartIndex = originalSql.replaceAll("(?i)ORDER\\s+BY", "ORDER BY").lastIndexOf("ORDER BY"); String orderStr = "ORDER BY n"; // 有排序,且是最外層的排序 if (orderStartIndex != -1 && originalSql.lastIndexOf(")") < orderStartIndex) { orderStr = originalSql.substring(orderStartIndex); } pageSql = originalSql.replaceFirst("(?i)select", "select * from (select row_number() over(" + orderStr + ") as rownumber,* from ( select top " + (rowBounds.getOffset() + rowBounds.getLimit()) + " n=0,"); pageSql += ")t )tt where rownumber> " + rowBounds.getOffset(); } return pageSql; } }
PageExecutor完整代碼:
package cn.com.common.page; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.BatchResult; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.ExecutorException; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.ParameterMode; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.transaction.Transaction; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; public class PageExecutor implements Executor { private Executor executor; private String pattern; public PageExecutor(Executor executor, String pattern) { this.executor = executor; this.pattern = pattern; } @Override public int update(MappedStatement ms, Object parameter) throws SQLException { return executor.update(ms, parameter); } @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException { RowBounds rb = new RowBounds(rowBounds.getOffset(), rowBounds.getLimit()); List<E> rows = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); return pageResolver(rows, ms, parameter, rb); } /** * 修改返回值類型 * @param rows * @param ms * @param parameter * @param rowBounds * @return */ private <E> List<E> pageResolver(List<E> rows, MappedStatement ms, Object parameter, RowBounds rowBounds) { String msid = ms.getId(); // 如果需要分頁查詢,修改返回類型為Page對象 if (msid.matches(pattern)) { int count = getCount(ms, parameter); int offset = rowBounds.getOffset(); int pagesize = rowBounds.getLimit(); return new Page<E>(offset/pagesize + 1, pagesize, count, rows); } return rows; } /** * 獲取總數 * @param ms * @param parameter * @return */ private int getCount(MappedStatement ms, Object parameter) { BoundSql bsql = ms.getBoundSql(parameter); String sql = bsql.getSql(); String countSql = getCountSql(sql); Connection connection = null; PreparedStatement stmt = null; ResultSet rs = null; try { connection = ms.getConfiguration().getEnvironment().getDataSource() .getConnection(); stmt = connection.prepareStatement(countSql); setParameters(stmt, ms, bsql, parameter); rs = stmt.executeQuery(); if (rs.next()) return rs.getInt(1); } catch (SQLException e) { e.printStackTrace(); } finally { try { if (connection != null && !connection.isClosed()) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } return 0; } @SuppressWarnings("unchecked") private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { Configuration configuration = mappedStatement.getConfiguration(); TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else { value = metaObject == null ? null : metaObject.getValue(propertyName); } @SuppressWarnings("rawtypes") TypeHandler typeHandler = parameterMapping.getTypeHandler(); if (typeHandler == null) { throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId()); } typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType()); } } } } private String getCountSql(String sql) { String countHql = " SELECT count(*) " + removeSelect(removeOrders(sql)); return countHql; } protected String removeOrders(String sql) { Pattern p = Pattern.compile("ORDER\\s*by[\\w|\\W|\\s|\\S]*", Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(sql); StringBuffer sb = new StringBuffer(); while (m.find()) { m.appendReplacement(sb, ""); } m.appendTail(sb); return sb.toString(); } // 去除sql語句中select子句 private static String removeSelect(String hql) { int beginPos = hql.toLowerCase().indexOf("from"); if (beginPos < 0) { throw new IllegalArgumentException(" hql : " + hql + " must has a keyword 'from'"); } return hql.substring(beginPos); } @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); return query(ms, parameter, rowBounds, resultHandler, executor.createCacheKey(ms, parameter, rowBounds, boundSql), boundSql); } @Override public List<BatchResult> flushStatements() throws SQLException { return executor.flushStatements(); } @Override public void commit(boolean required) throws SQLException { executor.commit(required); } @Override public void rollback(boolean required) throws SQLException { executor.rollback(required); } @Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { return executor .createCacheKey(ms, parameterObject, rowBounds, boundSql); } @Override public boolean isCached(MappedStatement ms, CacheKey key) { return executor.isCached(ms, key); } @Override public void clearLocalCache() { executor.clearLocalCache(); } @Override public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) { executor.deferLoad(ms, resultObject, property, key, targetType); } @Override public Transaction getTransaction() { return executor.getTransaction(); } @Override public void close(boolean forceRollback) { executor.close(forceRollback); } @Override public boolean isClosed() { return executor.isClosed(); } }
關於Page需要實現List接口的原因:可以看到,query方法返回值是List<E>,而我們現在要在dao中使用Page<E>對象來接收mybatis返回的結果,所以需要讓Page實現List接口。
分頁查詢執行順序:進入PageInterceptor的plugin方法,攔截到執行者,進入PageExecutor的query方法,執行executor.query()時,又再次回到PageInterceptor的plugin方法,這次會執行
進入intercept方法,將執行的sql拼接上分頁限制語句,然后查詢出數據結果集合。executor.query()執行完成后,繼續執行pageResolver,如果方法名稱和配置的需要執行分頁操作的字符串匹配時,查詢數據總量,並返回Page對象;如果不匹配,直接返回List對象。
四.xml配置:
<!-- myBatis文件 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:/conf/mybatis/mybaties-config.xml"></property> <property name="mapperLocations"> <list> <value>classpath:/conf/mybatis/**/*Mapper.xml</value> </list> </property> <property name="plugins"> <list> <ref bean="pageInterceptor"/> </list> </property> </bean> <!-- mybatis攔截器,實現分頁 --> <bean id="pageInterceptor" class="cn.com.common.page.PageInterceptor"> <property name="pattern" value="^.*page.*$"></property> <property name="dialect" value="mysql"></property> </bean>
五.測試代碼:
@Test public void testPage() { int pageNo = 1; int pageSize = 10; RowBounds bounds = new RowBounds((pageNo - 1) * pageSize, pageSize); Model record = new Model(); Page<Model> list = modelMapper.pageByConditions(bounds, record); }