百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

Mybatis 分页详解(mybatis分页语句)

sinye56 2024-10-07 14:29 4 浏览 0 评论

前言

在学习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<Student> queryStudentsBySql(@Param("offset") int offset, @Param("limit") int limit);

StudentMapper.xml 配置如下:

<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
 select * from student limit #{offset} , #{limit}
</select>

客户端使用的时候如下:

public List<Student> 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<Class<?>, Set<Method>> signatureMap; 
 
 private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { 
 this.target = target; 
 this.interceptor = interceptor; 
 this.signatureMap = signatureMap; 
 } 
 
 public static Object wrap(Object target, Interceptor interceptor) { 
 Map<Class<?>, Set<Method>> 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<Method> 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<Class<?>, Set<Method>> 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<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); 
 for (Signature sig : sigs) { 
 Set<Method> methods = signatureMap.get(sig.type()); 
 if (methods == null) { 
 methods = new HashSet<Method>(); 
 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<Class<?>, Set<Method>> signatureMap) { 
 Set<Class<?>> interfaces = new HashSet<Class<?>>(); 
 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<T> {
?
 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<T> 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<T> dtos){
 this.dtos = dtos;
 }
?
 public List<T> 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<ParameterMapping> 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;
 }
?
}

重点讲解:

  1. @Intercept注解中的@Signature中标示的属性,标示当前拦截器要拦截的那个类的那个方法,拦截方法的传入的参数
  2. 首先要明白,Mybatis是对JDBC的一个高层次的封装。而JDBC在完成数据操作的时候必须要有一个陈述对象。而陈述对应的SQL语句是在是在陈之前产生的。所以我们的思路就是在生成报表之前对SQL进行下手。更改SQL语句成我们需要的!
  3. 对于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();
 }
  1. 我们知道利用Mybatis查询一个集合时传入Rowbounds对象即可指定其Offset和Limit,只不过其没有利用原生sql去查询罢了,我们现在做的,就是通过拦截器拿到这个参数,然后织入到SQL语句中,这样我们就可以完成一个物理分页!

注册拦截器

在Spring文件中引入拦截器

 <!-- MyBatis 分页拦截器-->
 <bean id="paginationInterceptor" class="xx.xx.interceptor.PageInterceptor">
 <property name="dialect" value="mysql"/>
 </bean>
 
 ...
 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 <property name="dataSource" ref="dataSource"/>
 <!-- 自动扫描mapping.xml文件 -->
 <property name="mapperLocations" value="classpath*:evileye/*.xml"/>
 <property name="plugins" ref="paginationInterceptor"/>
 </bean>

分页定义的接口:

 List<Student> selectForSearch(PageDto<Student> pageDto);

客户端调用如下:

 PageDto pageDto = new PageDto<>();
 Student student =new Student();
 student.setId(1234);
 student.setName("sky");
 pageDto.setSearchCondition(student);

相关推荐

Linux基础知识之修改root用户密码

现象:Linux修改密码出现:Authenticationtokenmanipulationerror。故障解决办法:进入单用户,执行pwconv,再执行passwdroot。...

Linux如何修改远程访问端口

对于Linux服务器而言,其默认的远程访问端口为22。但是,出于安全方面的考虑,一般都会修改该端口。下面我来简答介绍一下如何修改Linux服务器默认的远程访问端口。对于默认端口而言,其相关的配置位于/...

如何批量更改文件的权限

如果你发觉一个目录结构下的大量文件权限(读、写、可执行)很乱时,可以执行以下两个命令批量修正:批量修改文件夹的权限chmod755-Rdir_name批量修改文件的权限finddir_nam...

CentOS「linux」学习笔记10:修改文件和目录权限

?linux基础操作:主要介绍了修改文件和目录的权限及chown和chgrp高级用法6.chmod修改权限1:字母方式[修改文件或目录的权限]u代表所属者,g代表所属组,o代表其他组的用户,a代表所有...

Linux下更改串口的权限

问题描述我在Ubuntu中使用ArduinoIDE,并且遇到串口问题。它过去一直有效,但由于可能不必要的原因,我觉得有必要将一些文件的所有权从root所有权更改为我的用户所有权。...

Linux chown命令:修改文件和目录的所有者和所属组

chown命令,可以认为是"changeowner"的缩写,主要用于修改文件(或目录)的所有者,除此之外,这个命令也可以修改文件(或目录)的所属组。当只需要修改所有者时,可使用...

chmod修改文件夹及子目录权限的方法

chmod修改文件夹及子目录权限的方法打开终端进入你需要修改的目录然后执行下面这条命令chmod777*-R全部子目录及文件权限改为777查看linux文件的权限:ls-l文件名称查看li...

Android 修改隐藏设置项权限

在Android系统中,修改某些隐藏设置项或权限通常涉及到系统级别的操作,尤其是针对非标准的、未在常规用户界面显示的高级选项。这些隐藏设置往往与隐私保护、安全相关的特殊功能有关,或者涉及开发者选项、权...

完蛋了!我不小心把Linux所有的文件权限修改了!在线等修复!

最近一个客户在群里说他一不小心把某台业务服务器的根目录权限给改了,本来想修改当前目录,结果执行成了根目录。...

linux改变安全性设置-改变所属关系

CentOS7.3学习笔记总结(五十八)-改变安全性设置-改变所属关系在以前的文章里,我介绍过linux文件权限,感兴趣的朋友可以关注我,阅读一下这篇文章。这里我们不在做过的介绍,注重介绍改变文件或者...

Python基础到实战一飞冲天(一)--linux基础(七)修改权限chmod

#07_Python基础到实战一飞冲天(一)--linux基础(七)--修改权限chmod-root-groupadd-groupdel-chgrp-username-passwd...

linux更改用户权限为root权限方法大全

背景在使用linux系统时,经常会遇到需要修改用户权限为root权限。通过修改用户所属群组groupid为root,此操作只能使普通用户实现享有部分root权限,普通用户仍不能像root用户一样享有超...

怎么用ip命令在linux中添加路由表项?

在Linux中添加路由表项,可以使用ip命令的route子命令。添加路由表项的基本语法如下:sudoiprouteadd<network>via<gateway>这...

Linux配置网络

1、网卡名配置相关文件回到顶部网卡名命名规则文件:/etc/udev/rules.d/70-persistent-net.rules#PCIdevice0x8086:0x100f(e1000)...

Linux系列---网络配置文件

1.网卡配置文件在/etc/sysconfig/network-scripts/下:[root@oldboynetwork-scripts]#ls/etc/sysconfig/network-s...

取消回复欢迎 发表评论: