Mybatis Plugin源码解析
课程目录
1、Mybatis之拦截器
1.1 查询大量数据引发问题
1.2 分页实现方式
1.3 Mybatis运行流程
1.4 拦截器概述
2、拦截器-入门程序
3、分页插件
一、查询大量数据引发的问题?
大家在使用比较常见的ORM框架【mybatis&hibernate等】,无非就是对数据库的增删改查操作。
而且使用的大多操作都是查询,当查询数据过多的时候我们一般会采用分页的形式展示数据:
1、性能问题
每次查询都查询出所有需求的数据对性能的影响非常大。
如果你的数据库中有一百万条记录,当我只需要查看10条数据时,这里有两种案例,你可以想一下哪一种更好。
-
查询出所有的记录,即一百万条,在编写业务代码从中获取10条记录回显到页面中
-
只查询出10条记录回显到页面中
可想而知,肯定是第二种方式最快
2、用户体验问题
如果一次性把所有数据展示出来,那么页面中非常占地方,从而导致用户体验不好。
二、常见的分页实现方式
刚才讲了为什么要分页,我们来看一下有哪些实现数据分页的手段【基于MySQL和mybatis】
常见的分页实现方式:
-
基于sql进行分页
-
通过拦截器进行分页(推荐)
三、实现方式的区别
1、使用sql分页
吸取了数组分页的教训,我们发现一次性读取所有数据,然后在程序中进行二次操作得到分页数据,会非常影响性能,所以,如果我们能直接从数据库中查询出分页数据,那么就解决了【系统性能、用户体验】问题,所以,sql分页横空出世。
缺陷:虽然sql分页解决了性能、用户体验问题,但是引发了另一个问题,我们直接查询分页数据时,sql后面都需要写limit语句,而且还需要写获取count的sql语句,从而导致sql语句的冗余问题。
2、使用拦截器进行分页
一句话概括:我们只需要关注我们的业务SQL,把分页业务交给别人来做,这样我们编码方便,分页功能也实现,那么这个别人就是拦截器,也就是我们所谓的插件。
注意:拦截器只负责拦截,至于拦截的业务还需要我们去编写,所以如果我们想要编写分页插件,那么我们需要考虑的两个问题就是:
1.在哪拦截
2.拦截业务
四、Mybatis执行原理
通过刚才所讲,我们如果想要实现分页插件,那么我们需要在Statement Handler之后进行拦截,而且,在mybatis中,拦截器可以拦截四个对象中的方法:Executor、Statement Handler、Parameter Handler、ResultSetHandler
五、自定义拦截器之入门程序
1.创建一个类,实现Interceptor接口
public class MyPlugin implements Interceptor {}
注意:Interceptor所属的包:org.apache.ibatis.plugin.Interceptor
2.重写抽象方法
/**
* @author 拦截器业务-核心
* @param invocation
* @return
* @throws Throwable
*/
public Object intercept(Invocation invocation) throws Throwable {
return null;
}
/**
* @author 用于提交拦截器,由拦截器链进行统一执行
* @param o
* @return
*/
public Object plugin(Object o) {
return null;
}
/**
* @author 用于设置参数
* @param properties
*/
public void setProperties(Properties properties) {
}
3.设置拦截对象
/**
* @Intercepts:定义拦截器注解
* @Signature:拦截器签名,用于指定拦截信息
* type:指定拦截对象
* method:指定拦截对象中的方法
* args:指定方法的参数
*/
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class}
)})
public class MyPlugin implements Interceptor {}
4.编写拦截器业务
/**
* @author 拦截器业务-核心
* @param invocation
* @return
* @throws Throwable
*/
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("这是拦截器,被执行了......");
return invocation.proceed();// 继续执行(放行)
}
5.提交拦截器
/**
* @author 用于提交拦截器,由拦截器链进行统一执行
* @param o :代理目标对象
* @return
*/
public Object plugin(Object o) {
// 提交拦截器以及代理目标对象(我们所写的拦截器)
return Plugin.wrap(o,this);
}
6.注册拦截器
<configuration>
<plugins>
<plugin interceptor="com.example.plugin.MyPlugin"></plugin>
</plugins>
</configuration>
7.测试
启动服务器,访问我们的user接口,查看在访问数据库时是否执行了我们的拦截器
通过刚刚所写的入门程序,我们实现了简单的拦截器,那么接下来我们可以想一下怎么用拦截器实现分页业务
六、分页插件
思路:
截取原始SQL语句,获取Count,拼接Limit
1.获取原始SQL语句
/** * @author 拦截器业务-核心 * @param invocation * @return * @throws Throwable */ public Object intercept(Invocation invocation) throws Throwable { // System.out.println("这是拦截器,被执行了......"); // 1.获取原始SQL语句 // 1.1 获取Statement Handler对象 // (在执行过程中,Executor会通过Statement Handler创建Statement对象,那么Sql语句由Statement Handler进行管理) StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); // 1.2 获取原始SQL语句 BoundSql:Sql语句对象 BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); System.out.println("原始Sql:"+sql); return invocation.proceed();// 继续执行(放行) }
2.获取参数
当执行mapper时,我们会传递条件参数以及分页数据,那么分页数据我们在拼接Limit时会用到
这里我封装了一个Page对象,当传递参数时,通过Map集合的方式进行传递,比如:
需求: 查询性别为男性的所有用户信息,每页显示10条记录 传递参数: Page page = new Page(); User user = new User(); Map<String,Object> map = HashMap<String,Object>(); map.put("page",page); map.put("user",user);
@Data @AllArgsConstructor @NoArgsConstructor public class Page { private int pageCount;// 总页数 private int thisPage;// 当前页 private int pageRow;// 每页行数 private int rowCount;// 总行数 }
3.获取StatementId
// 2.获取参数 Map<String,Object> map = (Map<String,Object>) boundSql.getParameterObject(); // 3.获取方法名,判断是否分页(其实获取的是Mapped Statement中的StatementId) // 3.1获取Mapped Statement对象 MetaObject metaObject = SystemMetaObject.forObject(statementHandler); MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // 3.2获取StatementId String statementId = mappedStatement.getId(); System.out.println("方法名:"+statementId);
4.是否分页
// 4.判断方法名是否以ByPage结尾 // true:分页 false:不分页 if(statementId.matches(".*ByPage$")){}
5.获取Count
// 4.判断方法名是否以ByPage结尾 // true:分页 false:不分页 if(statementId.matches(".*ByPage$")){ // 5.获取Count // 5.1定义countSql String countSql = "select count(0) from ("+sql+") a"; // 5.2利用JDBC操作数据库【可以封装方法】 Connection connection = (Connection) invocation.getArgs()[0]; PreparedStatement preparedStatement = connection.prepareStatement(countSql); // 注:为了防止有些时候原始sql中需要参数,我们需要获取Parameter Handler对象 ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler"); parameterHandler.setParameters(preparedStatement); ResultSet resultSet = preparedStatement.executeQuery(); if(resultSet.next()){ // 把count设置到Page对象中 page.setRowCount(resultSet.getInt(1)); } System.out.println("Page对象:"+page); resultSet.close(); preparedStatement.close(); }
6.拼接limit
// 6.拼接limit【可以封装方法】 StringBuffer sb = new StringBuffer(); sb.append(sql); // limit:当前页数-1*每页行数【可以封装方法到Page对象中】 sb.append(" limit "+((page.getThisPage()-1)*page.getPageRow()) + " , "+page.getPageRow()); // 把sql设置到上下文中继续执行 metaObject.setValue("delegate.boundSql.sql",sb.toString()); }
7.测试
当前页数:1 下一页:2 每页记录数:2
数据库记录: