前言
在学习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大型互联网技术的视频免费分享给大家