Mybatis 分頁詳解前言 在學習mybatis等持久層框架的時候,會經常對數據進行增刪改查操作,使用最多的是對數據庫進行查詢操作,如果查詢大量數據的時候,我們往往使用分頁進行查詢,也就是每次處理小部分數據,這樣對數據庫壓力就在可控范圍內。 分頁的幾種方式 1. 內存分頁 內存分頁的原理比較sb,就是一次性查詢數據庫中所有滿足條件的記錄,將這些數據臨時保存在集合中,再通過List的subList方法,獲取到滿足條件的記錄,由於太sb,直接忽略該種方式的分頁。 2. 物理分頁 在了解到通過內存分頁的缺陷后,我們發現不能每次都對數據庫中的所有數據都檢索。然后在程序中對獲取到的大量數據進行二次操作,這樣對空間和性能都是極大的損耗。所以我們希望能直接在數據庫語言中只檢索符合條件的記錄,不需要在通過程序對其作處理。這時,物理分頁技術橫空出世。 物理分頁是借助sql語句進行分頁,比如mysql是通過limit關鍵字,oracle是通過rownum等;其中mysql的分頁語法如下: select * from table limit 0,30 MyBatis 分頁 1.借助sql進行分頁 通過sql語句進行分頁的實現很簡單,我們先在StudentMapper接口中添加sql語句的查詢方法,如下: List queryStudentsBySql(@Param("offset") int offset, @Param("limit") int limit); StudentMapper.xml 配置如下: select * from student limit #{offset} , #{limit} 客戶端使用的時候如下: public List queryStudentsBySql(int offset, int pageSize) { return studentMapper.queryStudentsBySql(offset,pageSize); } sql分頁語句如下:select * from table limit index, pageSize; 缺點:雖然這里實現了按需查找,每次檢索得到的是指定的數據。但是每次在分頁的時候都需要去編寫limit語句,很冗余, 其次另外如果想知道總條數,還需要另外寫sql去統計查詢。而且不方便統一管理,維護性較差。所以我們希望能夠有一種更方便的分頁實現。 2. 攔截器分頁 攔截器的一個作用就是我們可以攔截某些方法的調用,我們可以選擇在這些被攔截的方法執行前后加上某些邏輯,也可以在執行這些被攔截的方法時執行自己的邏輯而不再執行被攔截的方法。Mybatis攔截器設計的一個初衷就是為了供用戶在某些時候可以實現自己的邏輯而不必去動Mybatis固有的邏輯。打個比方,對於Executor,Mybatis中有幾種實現:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。這個時候如果你覺得這幾種實現對於Executor接口的query方法都不能滿足你的要求,那怎么辦呢?是要去改源碼嗎?當然不。我們可以建立一個Mybatis攔截器用於攔截Executor接口的query方法,在攔截之后實現自己的query方法邏輯,之后可以選擇是否繼續執行原來的query方法。 Interceptor接口 對於攔截器Mybatis為我們提供了一個Interceptor接口,通過實現該接口就可以定義我們自己的攔截器。我們先來看一下這個接口的定義: package org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); } 我們可以看到在該接口中一共定義有三個方法,intercept、plugin和setProperties。plugin方法是攔截器用於封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理。當返回的是代理的時候我們可以對其中的方法進行攔截來調用intercept方法,當然也可以調用其他方法,這點將在后文講解。setProperties方法是用於在Mybatis配置文件中指定一些屬性的。 定義自己的Interceptor最重要的是要實現plugin方法和intercept方法,在plugin方法中我們可以決定是否要進行攔截進而決定要返回一個什么樣的目標對象。而intercept方法就是要進行攔截的時候要執行的方法。 對於plugin方法而言,其實Mybatis已經為我們提供了一個實現。Mybatis中有一個叫做Plugin的類,里面有一個靜態方法wrap(Object target,Interceptor interceptor),通過該方法可以決定要返回的對象是目標對象還是對應的代理。這里我們先來看一下Plugin的源碼: package org.apache.ibatis.plugin; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.ibatis.reflection.ExceptionUtil; public class Plugin implements InvocationHandler { private Object target; private Interceptor interceptor; private Map, Set> signatureMap; private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } public static Object wrap(Object target, Interceptor interceptor) { Map, Set> signatureMap = getSignatureMap(interceptor); Class type = target.getClass(); Class[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } private static Map, Set> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null) { // issue #251 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map, Set> signatureMap = new HashMap, Set>(); for (Signature sig : sigs) { Set methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) { Set> interfaces = new HashSet>(); while (type != null) { for (Class c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class[interfaces.size()]); } } 我們先看一下Plugin的wrap方法,它根據當前的Interceptor上面的注解定義哪些接口需要攔截,然后判斷當前目標對象是否有實現對應需要攔截的接口,如果沒有則返回目標對象本身,如果有則返回一個代理對象。而這個代理對象的InvocationHandler正是一個Plugin。所以當目標對象在執行接口方法時,如果是通過代理對象執行的,則會調用對應InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接着我們來看一下該invoke方法的內容。這里invoke方法的邏輯是:如果當前執行的方法是定義好的需要攔截的方法,則把目標對象、要執行的方法以及方法參數封裝成一個Invocation對象,再把封裝好的Invocation作為參數傳遞給當前攔截器的intercept方法。如果不需要攔截,則直接調用當前的方法。Invocation中定義了定義了一個proceed方法,其邏輯就是調用當前方法,所以如果在intercept中需要繼續調用當前方法的話可以調用invocation的procced方法。 這就是Mybatis中實現Interceptor攔截的一個思想,如果用戶覺得這個思想有問題或者不能完全滿足你的要求的話可以通過實現自己的Plugin來決定什么時候需要代理什么時候需要攔截。以下講解的內容都是基於Mybatis的默認實現即通過Plugin來管理Interceptor來講解的。 對於實現自己的Interceptor而言有兩個很重要的注解,一個是@Intercepts,其值是一個@Signature數組。@Intercepts用於表明當前的對象是一個Interceptor,而@Signature則表明要攔截的接口、方法以及對應的參數類型。 首先我們看一下攔截器的具體實現,在這里我們需要攔截所有以PageDto作為入參的所有查詢語句,自動以攔截器需要繼承Interceptor類,PageDto代碼如下: import java.util.Date; import java.util.List; /** * Created by chending on 16/3/27. */ public class PageDto { private Integer rows = 10; private Integer offset = 0; private Integer pageNo = 1; private Integer totalRecord = 0; private Integer totalPage = 1; private Boolean hasPrevious = false; private Boolean hasNext = false; private Date start; private Date end; private T searchCondition; private List dtos; public Date getStart() { return start; } public void setStart(Date start) { this.start = start; } public Date getEnd() { return end; } public void setEnd(Date end) { this.end = end; } public void setDtos(List dtos){ this.dtos = dtos; } public List getDtos(){ return dtos; } public Integer getRows() { return rows; } public void setRows(Integer rows) { this.rows = rows; } public Integer getOffset() { return offset; } public void setOffset(Integer offset) { this.offset = offset; } public Integer getPageNo() { return pageNo; } public void setPageNo(Integer pageNo) { this.pageNo = pageNo; } public Integer getTotalRecord() { return totalRecord; } public void setTotalRecord(Integer totalRecord) { this.totalRecord = totalRecord; } public T getSearchCondition() { return searchCondition; } public void setSearchCondition(T searchCondition) { this.searchCondition = searchCondition; } public Integer getTotalPage() { return totalPage; } public void setTotalPage(Integer totalPage) { this.totalPage = totalPage; } public Boolean getHasPrevious() { return hasPrevious; } public void setHasPrevious(Boolean hasPrevious) { this.hasPrevious = hasPrevious; } public Boolean getHasNext() { return hasNext; } public void setHasNext(Boolean hasNext) { this.hasNext = hasNext; } } 自定義攔截器PageInterceptor 代碼如下: import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Properties; import me.ele.elog.Log; import me.ele.elog.LogFactory; import me.ele.gaos.common.util.CommonUtil; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; 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.scripting.defaults.DefaultParameterHandler; /** * * 分頁攔截器,用於攔截需要進行分頁查詢的操作,然后對其進行分頁處理。 * */ @Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})}) public class PageInterceptor implements Interceptor { private String dialect = ""; //數據庫方言 private Log log = LogFactory.getLog(PageInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { if(invocation.getTarget() instanceof RoutingStatementHandler){ RoutingStatementHandler statementHandler = (RoutingStatementHandler)invocation.getTarget(); StatementHandler delegate = (StatementHandler) CommonUtil.getFieldValue(statementHandler, "delegate"); BoundSql boundSql = delegate.getBoundSql(); Object obj = boundSql.getParameterObject(); if (obj instanceof PageDto) { PageDto page = (PageDto) obj; //獲取delegate父類BaseStatementHandler的mappedStatement屬性 MappedStatement mappedStatement = (MappedStatement)CommonUtil.getFieldValue(delegate, "mappedStatement"); //攔截到的prepare方法參數是一個Connection對象 Connection connection = (Connection)invocation.getArgs()[0]; //獲取當前要執行的Sql語句 String sql = boundSql.getSql(); //給當前的page參數對象設置總記錄數 this.setTotalRecord(page, mappedStatement, connection); //給當前的page參數對象補全完整信息 //this.setPageInfo(page); //獲取分頁Sql語句 String pageSql = this.getPageSql(page, sql); //設置當前BoundSql對應的sql屬性為我們建立好的分頁Sql語句 CommonUtil.setFieldValue(boundSql, "sql", pageSql); } } return invocation.proceed(); } /** * 給當前的參數對象page設置總記錄數 * * @param page Mapper映射語句對應的參數對象 * @param mappedStatement Mapper映射語句 * @param connection 當前的數據庫連接 */ private void setTotalRecord(PageDto page, MappedStatement mappedStatement, Connection connection) throws Exception{ //獲取對應的BoundSql BoundSql boundSql = mappedStatement.getBoundSql(page); //獲取對應的Sql語句 String sql = boundSql.getSql(); //獲取計算總記錄數的sql語句 String countSql = this.getCountSql(sql); //通過BoundSql獲取對應的參數映射 List parameterMappings = boundSql.getParameterMappings(); //利用Configuration、查詢記錄數的Sql語句countSql、參數映射關系parameterMappings和參數對象page建立查詢記錄數對應的BoundSql對象。 BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page); //通過mappedStatement、參數對象page和BoundSql對象countBoundSql建立一個用於設定參數的ParameterHandler對象 ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql); //通過connection建立一個countSql對應的PreparedStatement對象。 PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = connection.prepareStatement(countSql); //通過parameterHandler給PreparedStatement對象設置參數 parameterHandler.setParameters(pstmt); //執行獲取總記錄數的Sql語句。 rs = pstmt.executeQuery(); if (rs.next()) { int totalRecord = rs.getInt(1); //給當前的參數page對象設置總記錄數 page.setTotalRecord(totalRecord); } } catch (SQLException e) { log.error(e); throw new SQLException(); } finally { try { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); } catch (SQLException e) { log.error(e); throw new SQLException(); } } } /** * 根據原Sql語句獲取對應的查詢總記錄數的Sql語句 * @param sql 原sql * @return 查詢總記錄數sql */ private String getCountSql(String sql) { int index = new String(sql).toLowerCase().indexOf("from"); return "select count(*) " + sql.substring(index); } /** * 給page對象補充完整信息 * * @param page page對象 */ private void setPageInfo(PageDto page) { Integer totalRecord = page.getTotalRecord(); Integer pageNo = page.getPageNo(); Integer rows = page.getRows(); //設置總頁數 Integer totalPage; if (totalRecord > rows) { if (totalRecord % rows == 0) { totalPage = totalRecord / rows; } else { totalPage = 1 + (totalRecord / rows); } } else { totalPage = 1; } page.setTotalPage(totalPage); //跳轉頁大於總頁數時,默認跳轉至最后一頁 if (pageNo > totalPage) { pageNo = totalPage; page.setPageNo(pageNo); } //設置是否有前頁 if(pageNo <= 1) { page.setHasPrevious(false); } else { page.setHasPrevious(true); } //設置是否有后頁 if(pageNo >= totalPage) { page.setHasNext(false); } else { page.setHasNext(true); } } /** * 根據page對象獲取對應的分頁查詢Sql語句 * 其它的數據庫都 沒有進行分頁 * * @param page 分頁對象 * @param sql 原sql語句 * @return 分頁sql */ private String getPageSql(PageDto page, String sql) { StringBuffer sqlBuffer = new StringBuffer(sql); if ("mysql".equalsIgnoreCase(dialect)) { //int offset = (page.getPageNo() - 1) * page.getRows(); sqlBuffer.append(" limit ").append(page.getOffset()).append(",").append(page.getRows()); return sqlBuffer.toString(); } return sqlBuffer.toString(); } /** * 攔截器對應的封裝原始對象的方法 */ @Override public Object plugin(Object arg0) { if (arg0 instanceof StatementHandler) { return Plugin.wrap(arg0, this); } else { return arg0; } } /** * 設置注冊攔截器時設定的屬性 */ @Override public void setProperties(Properties p) { } public String getDialect() { return dialect; } public void setDialect(String dialect) { this.dialect = dialect; } } 重點講解: @Intercept注解中的@Signature中標示的屬性,標示當前攔截器要攔截的那個類的那個方法,攔截方法的傳入的參數 首先要明白,Mybatis是對JDBC的一個高層次的封裝。而JDBC在完成數據操作的時候必須要有一個陳述對象。而陳述對應的SQL語句是在是在陳之前產生的。所以我們的思路就是在生成報表之前對SQL進行下手。更改SQL語句成我們需要的! 對於MyBatis的,其聲明的英文生成在RouteStatementHandler中。所以我們要做的就是攔截這個處理程序的prepare方法!然后修改的Sql語句! @Override public Object intercept(Invocation invocation) throws Throwable { // 其實就是代理模式! RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget(); StatementHandler delegate = (StatementHandler)ReflectUtil.getFieldValue(handler, "delegate"); String sql= delegate.getBoundSql().getSql(); return invocation.proceed(); } 我們知道利用Mybatis查詢一個集合時傳入Rowbounds對象即可指定其Offset和Limit,只不過其沒有利用原生sql去查詢罷了,我們現在做的,就是通過攔截器拿到這個參數,然后織入到SQL語句中,這樣我們就可以完成一個物理分頁! 注冊攔截器 在Spring文件中引入攔截器 ... 分頁定義的接口: List selectForSearch(PageDto pageDto); 客戶端調用如下: PageDto pageDto = new PageDto<>(); Student student =new Student(); student.setId(1234); student.setName("sky"); pageDto.setSearchCondition(student); |