MyBaits
是一个开源的优秀的持久层框架,SQL语句与代码分离,面向配置的编程,良好支持复杂数据映射,动态SQL; MyBatis
是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
通常使用 MyBatis
时使用以下几种形式进行分页:
物理分页: 在SQL里面使用LIMIT或者使用第三方插件(PageHelper等)
导入核心依赖:
<!-- SpringBoot MyBatis starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
搭建环境步骤可以参考: SpringBoot企业中常用starter
本次主要实现一个接口 org.apache.ibatis.plugin.Interceptor
,在接口中有3个方法为:
Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties);
setProperties 设置运行时mybatis核心配置参数方法。
Mybatis的拦截器实现机制,使用的是 JDK
的 InvocationHandler
,当我们调用 ParameterHandler,ResultSetHandler,StatementHandler,Executor
的对象的时候,实际上使用的是 Plugin
这个代理类的对象,这个类实现了InvocationHandler接口。
当我们调用 ParameterHandler,ResultSetHandler,StatementHandler,Executor
的对象的时候,
实际上使用的是 Plugin
这个代理类的对象,这个类实现了 InvocationHandler
接口。
Pager
实体对象,用来进行分页,实体内容如下: @Data @ToString public class Pager { /*当前页*/ private int page; /*每页大小*/ private int size; /*总记录*/ private long total; /*总页数*/ private int totalPage; /*自定义分页sql*/ private String customSQL; /*分页执行时长*/ private long executeTime; }
这里 customSQL
变量为自定义分页SQL,很多时候以为sql过于复杂关联了N张表,获取了N个字段会造成查询时间过于缓慢,加入这个字段主要是为了在一些复杂的SQL中不暴力使用默认的分页数量统计,可以自己根据SQL去除不需要的字段,已经不需要的表连接后的SQL来执行分页数量的统计。
定义分页处理接口 PagerHandler
,内容如下:
public interface PagerHandler { /** * 获取sql执行参数 * @param boundSql * @return */ public Pager getPager(BoundSql boundSql); /** * 执行分页 * * @param pager * @param boundSql * @param connection * @param metaObject * @return * @throws SQLException */ public Pager executer(Pager pager, BoundSql boundSql, Connection connection, MetaObject metaObject) throws SQLException; }
创建MyBats拦截器实现分页 PagerMyBatisInterceptor
,该类实现接口 org.apache.ibatis.plugin.Interceptor
和我们自己定义的 PagerHandler
,重写接口中方法。
我们还需要告诉MyBatis具体在什么地点进行拦截,使用 @Intercepts
来标注:
@Intercepts(value = { @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
主要拦截 StatementHandler
中 prepare
编译参数方法该方法需要传入参数类型为 Connection.class, Integer.class
,在 MyBatis 3.4.1
版本下参数只有一个 Connection
在类中定义一些常量:
private final Logger log = LoggerFactory.getLogger(PagerMyBatisInterceptor.class); private final int CONNECTION_INDEX = 0; //连接参数索引 private final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();//默认反射工厂 private final String DELEGATE_MAPPED_STATEMENT = "delegate.mappedStatement";//反射值获取路径 private final String DELEGATE_PARAMETER_HANDLER = "delegate.parameterHandler";//反射值获取路径
具体拦截器内容:
@Component @Intercepts(value = { @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class PagerMyBatisInterceptor implements PagerHandler, Interceptor { private final Logger log = LoggerFactory.getLogger(PagerMyBatisInterceptor.class); private final int CONNECTION_INDEX = 0; private final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory(); private final String DELEGATE_MAPPED_STATEMENT = "delegate.mappedStatement"; private final String DELEGATE_PARAMETER_HANDLER = "delegate.parameterHandler"; @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); //数据库连接 Connection connection = (Connection) args[CONNECTION_INDEX]; //负责处理Mybatis与JDBC之间Statement的交互 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); //MetaObject是Mybatis提供的一个用于方便、优雅访问对象属性的对象,通过它可以简化代码、不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。 MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY); //MappedStatement表示的是XML中的一个SQL MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(DELEGATE_MAPPED_STATEMENT); //SqlCommandType代表SQL类型 SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType(); //BoundSql则是其保存Sql语句的对象 BoundSql boundSql = statementHandler.getBoundSql(); //分页对象 Pager pager = getPager(boundSql); if (sqlCommandType.compareTo(SqlCommandType.SELECT) == 0 && pager != null) { executer(pager, boundSql, connection, metaObject); //执行查询 int left = (pager.getPage() - 1) * pager.getSize(); int right = pager.getSize(); String rewriteSql = boundSql.getSql() + " LIMIT " + left + "," + right; metaObject.setValue("boundSql.sql", rewriteSql); } long startTime = System.currentTimeMillis(); Object proceed = invocation.proceed(); long endTime = System.currentTimeMillis(); log.info("SQL TYPE [{}] , SQL EXECUTE TIME [{}] SQL:/n{}", sqlCommandType, startTime - endTime, boundSql.getSql().toUpperCase()); return proceed; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { //TODO 设置mybatis参数 } @Override public Pager getPager(BoundSql boundSql) { Object parameterObject = boundSql.getParameterObject(); if (parameterObject instanceof Pager) { return (Pager) parameterObject; } else if (parameterObject instanceof Map) { Map<String, Object> paramMap = (Map<String, Object>) parameterObject; Iterator<String> keys = paramMap.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); Object obj = paramMap.get(key); if (obj instanceof Pager) { return (Pager) obj; } } } return null; } @Override public Pager executer(Pager pager, BoundSql boundSql, Connection connection, MetaObject metaObject) throws SQLException { if (pager.getPage() == 0) { pager.setPage(0); } if (pager.getSize() == 0) { pager.setSize(0); } if (pager.getCustomSQL() == null) { //如果自己没有定义分页SQL,那么使用默认暴力分页 pager.setCustomSQL("SELECT COUNT(1) FROM (" + boundSql.getSql() + " ) tmp_table"); } // 预编译 PreparedStatement prepareStatement = connection.prepareStatement(pager.getCustomSQL()); // 预编译执行 ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue(DELEGATE_PARAMETER_HANDLER); parameterHandler.setParameters(prepareStatement); // 给sql语句设置参数 long startTime = System.currentTimeMillis(); ResultSet resultSet = prepareStatement.executeQuery(); long endTime = System.currentTimeMillis(); log.info("sql execute time {} sql:/n{}", startTime - endTime, pager.getCustomSQL().toUpperCase()); if (resultSet.next()) { long total = (long) resultSet.getObject(1);// 总记录数量 int totalPageNum = (int) ((total + pager.getSize() - 1) / pager.getSize()); pager.setTotal(total); pager.setTotalPage(totalPageNum); pager.setExecuteTime(startTime - endTime); } return pager; } }
通过方法 getPager
获取到pager对象,如果是Map参数那么就优先第一个通过引用的传递在 BoundSql
对象中获取到,然后执行分页获取里面的值进行计算,通过引用对象返回总记录数,总页数等。
在SpringBoot中如果需要使拦截器生效只需要在类型使用 @Component
将该类交给 Spring IOC
管理即可,至于拦截器顺序如:
有拦截器 PagerMyBatisInterceptor
与 OneInterceptor
,想要分页拦截器作为第二个拦截器只需要在类上标注 @ConditionalOnBean(OneInterceptor)
即可,在第一个拦截器实例化后再实例化第二个拦截器.
List<Map<String, Object>> selectUser(Pager pager); List<Map<String, Object>> selectUser(Map<String,Object> paramMap);
创建一个Pager对象传入即可。
本文源码地址:https://github.com/450255266/open-doubi/tree/master/spring-boot/custom-mybatis-pager