Mybatis 分页详解 mybatis怎么分页
bigegpt 2024-10-05 13:35 3 浏览
前言
在学习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; } ? }
重点讲解:
- @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文件中引入拦截器
<!-- 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);
相关推荐
- C#.NET Autofac 详解(c# autoit)
-
简介Autofac是一个成熟的、功能丰富的.NET依赖注入(DI)容器。相比于内置容器,它额外提供:模块化注册、装饰器(Decorator)、拦截器(Interceptor)、强o的属性/方法注...
- webapi 全流程(webapi怎么部署)
-
C#中的WebAPIMinimalApi没有控制器,普通api有控制器,MinimalApi是直达型,精简了很多中间代码,广泛适用于微服务架构MinimalApi一切都在组控制台应用程序类【Progr...
- .NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式
-
一:背景1.讲故事上一篇我们讲到了注解特性,harmony在内部提供了20个HarmonyPatch重载方法尽可能的让大家满足业务开发,那时候我也说了,特性虽然简单粗暴,但只能解决95%...
- C# 使用SemanticKernel调用本地大模型deepseek
-
一、先使用ollama部署好deepseek大模型。具体部署请看前面的头条使用ollama进行本地化部署deepseek大模型二、创建一个空的控制台dotnetnewconsole//添加依赖...
- C#.NET 中间件详解(.net core中间件use和run)
-
简介中间件(Middleware)是ASP.NETCore的核心组件,用于处理HTTP请求和响应的管道机制。它是基于管道模型的轻量级、模块化设计,允许开发者在请求处理过程中插入自定义逻辑。...
- IoC 自动注入:让依赖注册不再重复劳动
-
在ASP.NETCore中,IoC(控制反转)功能通过依赖注入(DI)实现。ASP.NETCore有一个内置的依赖注入容器,可以自动完成依赖注入。我们可以结合反射、特性或程序集扫描来实现自动...
- C#.NET 依赖注入详解(c#依赖注入的三种方式)
-
简介在C#.NET中,依赖注入(DependencyInjection,简称DI)是一种设计模式,用于实现控制反转(InversionofControl,IoC),以降低代码耦合、提高可...
- C#从零开始实现一个特性的自动注入功能
-
在现代软件开发中,依赖注入(DependencyInjection,DI)是实现松耦合、模块化和可测试代码的一个重要实践。C#提供了优秀的DI容器,如ASP.NETCore中自带的Micr...
- C#.NET 仓储模式详解(c#仓库货物管理系统)
-
简介仓储模式(RepositoryPattern)是一种数据访问抽象模式,它在领域模型和数据访问层之间创建了一个隔离层,使得领域模型无需直接与数据访问逻辑交互。仓储模式的核心思想是将数据访问逻辑封装...
- C#.NET 泛型详解(c# 泛型 滥用)
-
简介泛型(Generics)是指在类型或方法定义时使用类型参数,以实现类型安全、可重用和高性能的数据结构与算法为什么需要泛型类型安全防止“装箱/拆箱”带来的性能损耗,并在编译时检测类型错误。可重用同一...
- 数据分析-相关性分析(相关性 分析)
-
相关性分析是一种统计方法,用于衡量两个或多个变量之间的关系强度和方向。它通过计算相关系数来量化变量间的线性关系,从而帮助理解变量之间的相互影响。相关性分析常用于数据探索和假设检验,是数据分析和统计建模...
- geom_smooth()函数-R语言ggplot2快速入门18
-
在每节,先运行以下这几行程序。library(ggplot2)library(ggpubr)library(ggtext)#用于个性化图表library(dplyr)#用于数据处理p...
- 规范申报易错要素解析(规范申报易错要素解析)
-
为什么要规范申报?规范申报是以满足海关监管、征税、统计等工作为目的,纳税义务人及其代理人依法向海关如实申报的行为,也是海关审接单环节依法监管的重要工作。企业申报的内容须符合《中华人民共和国海关进出口货...
- 「Eurora」海关编码归类 全球海关编码查询 关务服务
-
海关编码是什么? 海关编码即HS编码,为编码协调制度的简称。 其全称为《商品名称及编码协调制度的国际公约》(InternationalConventionforHarmonizedCo...
- 9月1日起,河南省税务部门对豆制品加工业试行新政7类豆制品均适用投入产出法
-
全媒体记者杨晓川报道9月2日,记者从税务部门获悉,为减轻纳税人税收负担,完善农产品增值税进项税额抵扣机制,根据相关规定,结合我省实际情况,经广泛调查研究和征求意见,从9月1日起,我省税务部门对豆制品...
- 一周热门
- 最近发表
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)