语法
limit ${startPos},${pageSize}
limit 0,10; 代表从第0条数据开始 查10条数据, 第二页的时候就是 limit 10,10;
eg1: getByLimit
eg:2:查询第k名
查询最大、最小 ------------------ 使用聚合函数
查询第k名(升序、逆序)------ 使用order by 排序 + limit 进行挑选。
查询某个字段第k名的数据。注意是k-1(从0开始!)
select * from 表名 order by 字段 desc/asc limit k-1, 1
中小数据量时,可以使用联合索引(where限制条件和orderby排序的条件组合)提升查询速度。
数据量增加,limit语句的偏移量会越大,速度会慢,可以通过子查询提升分页效率
select *
from attend_lesson_record
where id >= (select id from attend_lesson_record limit 10, 1) limit 10
省略了limit的内容, 属于逻辑分页,即实际上sql查询的是所有的数据,在业务层进行了分页而已,比较占用内存,而且数据更新不及时,可能会有一定的滞后性!不推荐使用!
RowBounds对象有2个属性,offset和limit。
因此,取出来的数据就是:从第offset+1行开始,取limit行
org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap()方法源码。
Mybatis中使用RowBounds实现分页的大体思路:
先取出所有数据,然后游标移动到offset位置,循环取limit条数据,然后把剩下的数据舍弃。
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
//跳过RowBounds设置的offset值
skipRows(rsw.getResultSet(), rowBounds);
//判断数据是否小于limit,如果小于limit的话就不断的循环取值
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
//判断数据是否小于limit,小于返回true
return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}
//跳过不需要的行,应该就是rowbounds设置的limit和offset
private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
rs.absolute(rowBounds.getOffset());
}
} else {
//跳过RowBounds中设置的offset条数据,只能逐条滚动到指定位置
for (int i = 0; i < rowBounds.getOffset(); i++) {
rs.next();
}
}
}
eg:getByRowBounds
xml的sql查询全部,调用sql的方法传入 rowbounds
RowBounds rowbounds = new RowBounds(offset, Integer.parseInt(pageSize));
问题:select * from user where id>0 limit 0,10
RowBounds会将id>0的所有数据全都加载到内存中,然后截取前10行,若id>0有100万条,则100万条数据都会加载到内存中,从而造成内存OOM。
依赖
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>5.1.7version>
dependency>
核心配置
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor" />
plugins>
eg
@Override
public List<AttendLessonRecord> getByPageHelper(Integer pageNum, Integer pageSize) {
/**
* 几种写法
*/
// //doSelectPage 创建接口
// Page page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
// @Override
// public void doSelect() {
// attendLessonRecordMapper.getAllRecord();
// }
// });
// //doSelectPage lambda写法
// PageHelper.startPage(pageNum, pageSize).doSelectPage(()-> attendLessonRecordMapper.getAllRecord());
//
// //doSelectPageInfo 创建接口
// PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
// @Override
// public void doSelect() {
// attendLessonRecordMapper.getAllRecord();
// }
// });
// //doSelectPageInfo jdk8 lambda 表达式
// PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(()-> attendLessonRecordMapper.getAllRecord());
PageHelper.startPage(pageNum, pageSize);
return attendLessonRecordMapper.getAllRecord();
}
执行顺序
拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行,用于对处理器进行预处理和后处理。在Spring MVC 与Spring Boot 中使用拦截器一般是实现HandlerInterceptor
接口。
作用:将多个控制器中共有代码放入拦截器可以减少控制器代码冗余,可以构成拦截器栈,完成特定功能。比如日志记录、登录判断、权限检查等作用。
执行流程:
(1)、程序先执行preHandle()方法,如果该方法的返回值为true,则程序会继续向下执行处理器中的方法,否则将不再向下执行;
(2)、在业务处理器(即控制器Controller类)处理完请求后,会执行postHandle()方法,然后会通过DispatcherServlet向客户端返回响应;
(3)、在DispatcherServlet处理完请求后,才会执行afterCompletion()方法。
HandlerInterceptor
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface HandlerInterceptor {
//这个方法可以实现处理器的预处理,也就是它会在handler 方法执行之前就开始执行。当返回值是true 时表示继续执行,返回false 时则不会执行后续的拦截器或处理器。作用:身份验证,身份授权等。
boolean preHandle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception;
//这个方法是后处理回调方法,也就是在控制器完成后(试图渲染之前)执行。作用:将公用的模型数据传到视图,也可以在这里统一指定视图(菜单导航等)。
void postHandle(HttpServletRequest var1, HttpServletResponse var2, Object var3, ModelAndView var4) throws Exception;
//这个方法是请求处理完毕后的回调方法,即在视图渲染完毕时调用。作用:进行统一的异常处理,日志处理等。
void afterCompletion(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4) throws Exception;
}
注册拦截器:
编写一个类继承WebMvcConfigurationSupport(或实现 WebMvcConfigurer 接口),并重写addInterceptors来注册拦截器。
多个拦截器的执行顺序取决于拦截器注册的顺序。
应用场景
登录验证,判断用户是否登录
权限验证,判断用户是否有权限访问资源,比如校验token
日志记录,记录请求操作日志(用户IP,访问时间等),以便统计请求访问量
处理cookie、本地化、国际化、主题等
性能监控,监控请求处理时长等
通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有比如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)
示例
TestLogInterceptor
MDDTestController
TestWebMvcConfigurer
原理:
(拦截器,过滤器,监听器)
应用背景:分页操作,数据权限过滤操作,SQL执行时间性能监控等等, 拦截sql,达到不入侵原有代码业务处理一些东西,用到Mybatis的拦截器Interceptor
从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:
执行过程
Mybatis拦截器原理
Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理
默认拦截器,自定义拦截器
顺序:
拦截器的原理核心的内容在于拦截器配置解析、拦截器的职责链构造、拦截器的执行,串联上述功能后就能传统整体的流程。
配置文件解析
通过XML配置,原始的XMLConfigBuilder 构建configuration添加拦截器
拦截器链,初始化配置文件的时候就把所有的拦截器添加到拦截器链中
org.apache.ibatis.plugin.InterceptorChain
pluginAll:织入拦截器 – 循环调用每个Interceptor.plugin方法,添加拦截器
addInterceptor:保存拦截器
getInterceptors:获取拦截器
拦截器织入依赖于JDK自带的Proxy.newProxyInstance动态代理来实现。
返回的对象是个Plugin对象的动态代理。
通过解析注解@Intercepts注解@Signature来构建class + method两个维度的map对象。
以@Signature(type = ParameterHandler.class, method = “setParameters”, args = PreparedStatement.class)为例,会构建key为ParameterHandler,value为setParameters的map
针对ParameterHandler 、ResultSetHandler 、StatementHandler 、Executor 的拦截器的织入都是通过interceptorChain.pluginAll来实现。
mybatis 在实例化Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口对象的时候调用interceptorChain.pluginAll() 方法插入进去的。
其实就是循环执行拦截器链所有的拦截器的plugin() 方法,mybatis官方推荐的plugin方法是Plugin.wrap() 方法,这个类就是上面的TargetProxy类
wrap织入单个拦截器
org.apache.ibatis.session.Configuration
产生3种执行器 BatchExecutor/ReuseExecutor/SimpleExecutor
拦截器执行
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
@Override
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)) {
// 由拦截器interceptor负责处理
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
查询方法:PageHelper.startPage(pageNum, pageSize, orderBy);
创建Page对象,后调用查询数据库方法查询
PageInterceptor
String getPageSql(MappedStatement var1, BoundSql var2, Object var3, RowBounds var4, CacheKey var5);
getByPageHelperLocation
startPage()方法调用了Thread.currentThred()方法,也就是说,使用startPage()方法,会在该方法下的第一条sql语句执行分页操作,这也就是为什么导致两条sql分页不起作用的原因,最后,建议将startPage()方法放在service实现层,即所需要的结果的sql之前。
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
PageHelper.startPage 对最近的查询语句生效
PageHelper 使用了静态的 ThreadLocal 参数,让线程绑定了分页参数, 这个参数如果没被使用就会一直留在那儿,当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
unsafePageHelper
让这个分页参数紧跟查询,可以查就建立;也就是保证被消费;或者在 finally 调用 PageHelper.clearPage(); 清除。
PageHelper使用了静态的ThreadLocal参数,分页参数和线程是绑定的;当分页参数没有被消费时,会一直存在threadlocal中,在下一次执行的sql中会拼接这些参数。
分页参数紧跟 list 查询。如果先写分页,又写了别的判断逻辑,没有执行 list 查询时,那么分页参数就会在threadlocal中,下次执行sql会消费这些参数,就会导致“不安全分页”。
错误原因:丢失了真实类型,计算总记录条数的时候 ,计算了lis的个数,没有从page对象中获取总记录数
改动:创建新的PageInfo对象需要拷贝里面的 属性