Mybatis 分頁詳解

前言

在學習mybatis等持久層框架的時候,會經(jīng)常對數(shù)據(jù)進行增刪改查操作,使用最多的是對數(shù)據(jù)庫進行查詢操作,如果查詢大量數(shù)據(jù)的時候,我們往往使用分頁進行查詢,也就是每次處理小部分數(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ù)進行二次操作,這樣對空間和性能都是極大的損耗。所以我們希望能直接在數(shù)據(jù)庫語言中只檢索符合條件的記錄,不需要在通過程序?qū)ζ渥魈幚?。這時,物理分頁技術(shù)橫空出世。

物理分頁是借助sql語句進行分頁,比如mysql是通過limit關(guān)鍵字,oracle是通過rownum等;其中mysql的分頁語法如下:

select * from table limit 0,30

MyBatis 分頁

1.借助sql進行分頁

通過sql語句進行分頁的實現(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)一管理,維護性較差。所以我們希望能夠有一種更方便的分頁實現(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方法都不能滿足你的要求,那怎么辦呢?是要去改源碼嗎?當然不。我們可以建立一個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方法是攔截器用于封裝目標對象的,通過該方法我們可以返回目標對象本身,也可以返回一個它的代理。當返回的是代理的時候我們可以對其中的方法進行攔截來調(diào)用intercept方法,當然也可以調(diào)用其他方法,這點將在后文講解。setProperties方法是用于在Mybatis配置文件中指定一些屬性的。

定義自己的Interceptor最重要的是要實現(xiàn)plugin方法和intercept方法,在plugin方法中我們可以決定是否要進行攔截進而決定要返回一個什么樣的目標對象。而intercept方法就是要進行攔截的時候要執(zhí)行的方法。

對于plugin方法而言,其實Mybatis已經(jīng)為我們提供了一個實現(xiàn)。Mybatis中有一個叫做Plugin的類,里面有一個靜態(tài)方法wrap(Object target,Interceptor interceptor),通過該方法可以決定要返回的對象是目標對象還是對應(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ù)當前的Interceptor上面的注解定義哪些接口需要攔截,然后判斷當前目標對象是否有實現(xiàn)對應(yīng)需要攔截的接口,如果沒有則返回目標對象本身,如果有則返回一個代理對象。而這個代理對象的InvocationHandler正是一個Plugin。所以當目標對象在執(zhí)行接口方法時,如果是通過代理對象執(zhí)行的,則會調(diào)用對應(yīng)InvocationHandler的invoke方法,也就是Plugin的invoke方法。所以接著我們來看一下該invoke方法的內(nèi)容。這里invoke方法的邏輯是:如果當前執(zhí)行的方法是定義好的需要攔截的方法,則把目標對象、要執(zhí)行的方法以及方法參數(shù)封裝成一個Invocation對象,再把封裝好的Invocation作為參數(shù)傳遞給當前攔截器的intercept方法。如果不需要攔截,則直接調(diào)用當前的方法。Invocation中定義了定義了一個proceed方法,其邏輯就是調(diào)用當前方法,所以如果在intercept中需要繼續(xù)調(diào)用當前方法的話可以調(diào)用invocation的procced方法。

這就是Mybatis中實現(xiàn)Interceptor攔截的一個思想,如果用戶覺得這個思想有問題或者不能完全滿足你的要求的話可以通過實現(xiàn)自己的Plugin來決定什么時候需要代理什么時候需要攔截。以下講解的內(nèi)容都是基于Mybatis的默認實現(xiàn)即通過Plugin來管理Interceptor來講解的。

對于實現(xiàn)自己的Interceptor而言有兩個很重要的注解,一個是@Intercepts,其值是一個@Signature數(shù)組。@Intercepts用于表明當前的對象是一個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;

?

?

/**

*

* 分頁攔截器,用于攔截需要進行分頁查詢的操作,然后對其進行分頁處理。

*

*/

@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];

//獲取當前要執(zhí)行的Sql語句

String sql = boundSql.getSql();

//給當前的page參數(shù)對象設(shè)置總記錄數(shù)

this.setTotalRecord(page, mappedStatement, connection);

//給當前的page參數(shù)對象補全完整信息

//this.setPageInfo(page);

//獲取分頁Sql語句

String pageSql = this.getPageSql(page, sql);

//設(shè)置當前BoundSql對應(yīng)的sql屬性為我們建立好的分頁Sql語句

CommonUtil.setFieldValue(boundSql, "sql", pageSql);

}

}

return invocation.proceed();

}

?

/**

* 給當前的參數(shù)對象page設(shè)置總記錄數(shù)

*

* @param page Mapper映射語句對應(yīng)的參數(shù)對象

* @param mappedStatement Mapper映射語句

* @param connection 當前的數(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);

//給當前的參數(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對象補充完整信息

*

* @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ù)時,默認跳轉(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ù)庫都 沒有進行分頁

*

* @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中標示的屬性,標示當前攔截器要攔截的那個類的那個方法,攔截方法的傳入的參數(shù)

首先要明白,Mybatis是對JDBC的一個高層次的封裝。而JDBC在完成數(shù)據(jù)操作的時候必須要有一個陳述對象。而陳述對應(yīng)的SQL語句是在是在陳之前產(chǎn)生的。所以我們的思路就是在生成報表之前對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去查詢罷了,我們現(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);

如果想學習Java工程化、高性能及分布式、深入淺出。性能調(diào)優(yōu)、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java高級架構(gòu)進階群:180705916,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容