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);
如果想學習Java工程化、高效能及分散式、深入淺出。效能調優、Spring,MyBatis,Netty原始碼分析的朋友可以加我的Java高階架構進階群:180705916,群裡有阿里大牛直播講解技術,以及Java大型網際網路技術的視訊免費分享給大家