前言
在學(xué)習(xí)mybatis等持久層框架的時候羹幸,會經(jīng)常對數(shù)據(jù)進(jìn)行增刪改查操作脊髓,使用最多的是對數(shù)據(jù)庫進(jìn)行查詢操作,如果查詢大量數(shù)據(jù)的時候睹欲,我們往往使用分頁進(jìn)行查詢供炼,也就是每次處理小部分?jǐn)?shù)據(jù),這樣對數(shù)據(jù)庫壓力就在可控范圍內(nèi)窘疮。
分頁的幾種方式
1. 內(nèi)存分頁
內(nèi)存分頁的原理比較sb袋哼,就是一次性查詢數(shù)據(jù)庫中所有滿足條件的記錄,將這些數(shù)據(jù)臨時保存在集合中闸衫,再通過List的subList方法涛贯,獲取到滿足條件的記錄,由于太sb蔚出,直接忽略該種方式的分頁弟翘。
2. 物理分頁
在了解到通過內(nèi)存分頁的缺陷后,我們發(fā)現(xiàn)不能每次都對數(shù)據(jù)庫中的所有數(shù)據(jù)都檢索骄酗。然后在程序中對獲取到的大量數(shù)據(jù)進(jìn)行二次操作稀余,這樣對空間和性能都是極大的損耗。所以我們希望能直接在數(shù)據(jù)庫語言中只檢索符合條件的記錄趋翻,不需要在通過程序?qū)ζ渥魈幚砭α铡_@時,物理分頁技術(shù)橫空出世。
物理分頁是借助sql語句進(jìn)行分頁师骗,比如mysql是通過limit關(guān)鍵字历等,oracle是通過rownum等;其中mysql的分頁語法如下:
select * from table limit 0,30
MyBatis 分頁
1.借助sql進(jìn)行分頁
通過sql語句進(jìn)行分頁的實現(xiàn)很簡單,我們先在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;
缺點:雖然這里實現(xiàn)了按需查找寒屯,每次檢索得到的是指定的數(shù)據(jù)。但是每次在分頁的時候都需要去編寫limit語句黍少,很冗余, 其次另外如果想知道總條數(shù)寡夹,還需要另外寫sql去統(tǒng)計查詢。而且不方便統(tǒng)一管理厂置,維護(hù)性較差要出。所以我們希望能夠有一種更方便的分頁實現(xiàn)。
2. 攔截器分頁
攔截器的一個作用就是我們可以攔截某些方法的調(diào)用农渊,我們可以選擇在這些被攔截的方法執(zhí)行前后加上某些邏輯,也可以在執(zhí)行這些被攔截的方法時執(zhí)行自己的邏輯而不再執(zhí)行被攔截的方法或颊。Mybatis攔截器設(shè)計的一個初衷就是為了供用戶在某些時候可以實現(xiàn)自己的邏輯而不必去動Mybatis固有的邏輯砸紊。打個比方,對于Executor囱挑,Mybatis中有幾種實現(xiàn):BatchExecutor醉顽、ReuseExecutor、SimpleExecutor和CachingExecutor平挑。這個時候如果你覺得這幾種實現(xiàn)對于Executor接口的query方法都不能滿足你的要求游添,那怎么辦呢?是要去改源碼嗎通熄?當(dāng)然不唆涝。我們可以建立一個Mybatis攔截器用于攔截Executor接口的query方法,在攔截之后實現(xiàn)自己的query方法邏輯唇辨,之后可以選擇是否繼續(xù)執(zhí)行原來的query方法廊酣。
Interceptor接口
對于攔截器Mybatis為我們提供了一個Interceptor接口,通過實現(xiàn)該接口就可以定義我們自己的攔截器赏枚。我們先來看一下這個接口的定義:
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方法是攔截器用于封裝目標(biāo)對象的凡辱,通過該方法我們可以返回目標(biāo)對象本身,也可以返回一個它的代理栗恩。當(dāng)返回的是代理的時候我們可以對其中的方法進(jìn)行攔截來調(diào)用intercept方法透乾,當(dāng)然也可以調(diào)用其他方法,這點將在后文講解。setProperties方法是用于在Mybatis配置文件中指定一些屬性的续徽。
定義自己的Interceptor最重要的是要實現(xiàn)plugin方法和intercept方法蚓曼,在plugin方法中我們可以決定是否要進(jìn)行攔截進(jìn)而決定要返回一個什么樣的目標(biāo)對象。而intercept方法就是要進(jìn)行攔截的時候要執(zhí)行的方法钦扭。
對于plugin方法而言纫版,其實Mybatis已經(jīng)為我們提供了一個實現(xiàn)。Mybatis中有一個叫做Plugin的類客情,里面有一個靜態(tài)方法wrap(Object target,Interceptor interceptor)其弊,通過該方法可以決定要返回的對象是目標(biāo)對象還是對應(yīng)的代理。這里我們先來看一下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方法膀斋,它根據(jù)當(dāng)前的Interceptor上面的注解定義哪些接口需要攔截梭伐,然后判斷當(dāng)前目標(biāo)對象是否有實現(xiàn)對應(yīng)需要攔截的接口,如果沒有則返回目標(biāo)對象本身仰担,如果有則返回一個代理對象糊识。而這個代理對象的InvocationHandler正是一個Plugin。所以當(dāng)目標(biāo)對象在執(zhí)行接口方法時摔蓝,如果是通過代理對象執(zhí)行的赂苗,則會調(diào)用對應(yīng)InvocationHandler的invoke方法,也就是Plugin的invoke方法贮尉。所以接著我們來看一下該invoke方法的內(nèi)容拌滋。這里invoke方法的邏輯是:如果當(dāng)前執(zhí)行的方法是定義好的需要攔截的方法,則把目標(biāo)對象猜谚、要執(zhí)行的方法以及方法參數(shù)封裝成一個Invocation對象败砂,再把封裝好的Invocation作為參數(shù)傳遞給當(dāng)前攔截器的intercept方法。如果不需要攔截魏铅,則直接調(diào)用當(dāng)前的方法昌犹。Invocation中定義了定義了一個proceed方法,其邏輯就是調(diào)用當(dāng)前方法览芳,所以如果在intercept中需要繼續(xù)調(diào)用當(dāng)前方法的話可以調(diào)用invocation的procced方法祭隔。
這就是Mybatis中實現(xiàn)Interceptor攔截的一個思想,如果用戶覺得這個思想有問題或者不能完全滿足你的要求的話可以通過實現(xiàn)自己的Plugin來決定什么時候需要代理什么時候需要攔截路操。以下講解的內(nèi)容都是基于Mybatis的默認(rèn)實現(xiàn)即通過Plugin來管理Interceptor來講解的疾渴。
對于實現(xiàn)自己的Interceptor而言有兩個很重要的注解,一個是@Intercepts屯仗,其值是一個@Signature數(shù)組搞坝。@Intercepts用于表明當(dāng)前的對象是一個Interceptor,而@Signature則表明要攔截的接口魁袜、方法以及對應(yīng)的參數(shù)類型桩撮。
首先我們看一下攔截器的具體實現(xiàn)敦第,在這里我們需要攔截所有以PageDto作為入?yún)⒌乃胁樵冋Z句,自動以攔截器需要繼承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;
?
?
/**
*
* 分頁攔截器芜果,用于攔截需要進(jìn)行分頁查詢的操作,然后對其進(jìn)行分頁處理融师。
*
*/
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})
public class PageInterceptor implements Interceptor {
private String dialect = ""; //數(shù)據(jù)庫方言
?
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方法參數(shù)是一個Connection對象
Connection connection = (Connection)invocation.getArgs()[0];
//獲取當(dāng)前要執(zhí)行的Sql語句
String sql = boundSql.getSql();
//給當(dāng)前的page參數(shù)對象設(shè)置總記錄數(shù)
this.setTotalRecord(page, mappedStatement, connection);
//給當(dāng)前的page參數(shù)對象補(bǔ)全完整信息
//this.setPageInfo(page);
//獲取分頁Sql語句
String pageSql = this.getPageSql(page, sql);
//設(shè)置當(dāng)前BoundSql對應(yīng)的sql屬性為我們建立好的分頁Sql語句
CommonUtil.setFieldValue(boundSql, "sql", pageSql);
}
}
return invocation.proceed();
}
?
/**
* 給當(dāng)前的參數(shù)對象page設(shè)置總記錄數(shù)
*
* @param page Mapper映射語句對應(yīng)的參數(shù)對象
* @param mappedStatement Mapper映射語句
* @param connection 當(dāng)前的數(shù)據(jù)庫連接
*/
private void setTotalRecord(PageDto page, MappedStatement mappedStatement, Connection connection) throws Exception{
//獲取對應(yīng)的BoundSql
BoundSql boundSql = mappedStatement.getBoundSql(page);
//獲取對應(yīng)的Sql語句
String sql = boundSql.getSql();
//獲取計算總記錄數(shù)的sql語句
String countSql = this.getCountSql(sql);
//通過BoundSql獲取對應(yīng)的參數(shù)映射
List parameterMappings = boundSql.getParameterMappings();
//利用Configuration右钾、查詢記錄數(shù)的Sql語句countSql摧茴、參數(shù)映射關(guān)系parameterMappings和參數(shù)對象page建立查詢記錄數(shù)對應(yīng)的BoundSql對象容握。
BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
//通過mappedStatement拷姿、參數(shù)對象page和BoundSql對象countBoundSql建立一個用于設(shè)定參數(shù)的ParameterHandler對象
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);
//通過connection建立一個countSql對應(yīng)的PreparedStatement對象逆巍。
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = connection.prepareStatement(countSql);
//通過parameterHandler給PreparedStatement對象設(shè)置參數(shù)
parameterHandler.setParameters(pstmt);
//執(zhí)行獲取總記錄數(shù)的Sql語句。
rs = pstmt.executeQuery();
if (rs.next()) {
int totalRecord = rs.getInt(1);
//給當(dāng)前的參數(shù)page對象設(shè)置總記錄數(shù)
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();
}
}
}
?
/**
* 根據(jù)原Sql語句獲取對應(yīng)的查詢總記錄數(shù)的Sql語句
* @param sql 原sql
* @return 查詢總記錄數(shù)sql
*/
private String getCountSql(String sql) {
int index = new String(sql).toLowerCase().indexOf("from");
return "select count(*) " + sql.substring(index);
}
?
/**
* 給page對象補(bǔ)充完整信息
*
* @param page page對象
*/
private void setPageInfo(PageDto page) {
Integer totalRecord = page.getTotalRecord();
Integer pageNo = page.getPageNo();
Integer rows = page.getRows();
?
//設(shè)置總頁數(shù)
Integer totalPage;
if (totalRecord > rows) {
if (totalRecord % rows == 0) {
totalPage = totalRecord / rows;
} else {
totalPage = 1 + (totalRecord / rows);
}
} else {
totalPage = 1;
}
page.setTotalPage(totalPage);
?
//跳轉(zhuǎn)頁大于總頁數(shù)時,默認(rèn)跳轉(zhuǎn)至最后一頁
if (pageNo > totalPage) {
pageNo = totalPage;
page.setPageNo(pageNo);
}
?
//設(shè)置是否有前頁
if(pageNo <= 1) {
page.setHasPrevious(false);
} else {
page.setHasPrevious(true);
}
?
//設(shè)置是否有后頁
if(pageNo >= totalPage) {
page.setHasNext(false);
} else {
page.setHasNext(true);
}
}
?
/**
* 根據(jù)page對象獲取對應(yīng)的分頁查詢Sql語句
* 其它的數(shù)據(jù)庫都 沒有進(jìn)行分頁
*
* @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();
}
?
/**
* 攔截器對應(yīng)的封裝原始對象的方法
*/
@Override
public Object plugin(Object arg0) {
?
if (arg0 instanceof StatementHandler) {
return Plugin.wrap(arg0, this);
} else {
return arg0;
}
}
?
/**
* 設(shè)置注冊攔截器時設(shè)定的屬性
*/
@Override
public void setProperties(Properties p) {
?
}
?
public String getDialect() {
return dialect;
}
?
public void setDialect(String dialect) {
this.dialect = dialect;
}
?
}
重點講解:
@Intercept注解中的@Signature中標(biāo)示的屬性忿晕,標(biāo)示當(dāng)前攔截器要攔截的那個類的那個方法绍妨,攔截方法的傳入的參數(shù)
首先要明白志秃,Mybatis是對JDBC的一個高層次的封裝房待。而JDBC在完成數(shù)據(jù)操作的時候必須要有一個陳述對象邢羔。而陳述對應(yīng)的SQL語句是在是在陳之前產(chǎn)生的。所以我們的思路就是在生成報表之前對SQL進(jìn)行下手桑孩。更改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去查詢罷了,我們現(xiàn)在做的诡右,就是通過攔截器拿到這個參數(shù)安岂,然后織入到SQL語句中,這樣我們就可以完成一個物理分頁帆吻!
注冊攔截器
在Spring文件中引入攔截器
...
分頁定義的接口:
List selectForSearch(PageDto pageDto);
客戶端調(diào)用如下:
PageDto pageDto = new PageDto<>();
Student student =new Student();
student.setId(1234);
student.setName("sky");
pageDto.setSearchCondition(student);
如果想學(xué)習(xí)Java工程化域那、高性能及分布式、深入淺出猜煮。性能調(diào)優(yōu)次员、Spring,MyBatis王带,Netty源碼分析的朋友可以加我的Java高級架構(gòu)進(jìn)階群:180705916淑蔚,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家