前言
这是一篇简单总结,mybatis分页的文章。
mybatis简单了解
在很久以前,我们会使用jdbc对数据库进行crud,随着ORM框架的诞生,为了效率,可能就选择了hibernate和mybatis等技术。hibernate由于比较笨重,虽然切换数据库也不会太大改变我们的程序,但是也不是太灵活,比如我们无法利用数据库比较重要的索引等技巧,所以现在很多项目中都是使用mybais的。今天只聊mybatis的分页技术。
分页类型
物理分页:直接从数据库中拿出我们需要的数据,例如在Mysql中使用limit。
逻辑分页:从数据库中拿出所有符合要求的数据,然后再从这些数据中拿到我们需要的分页数据。
分页方式
mybatis中几种写法:
1.数组分页
这种的思路是一次性查询所有数据,得到一个集合,然后在根据页码等参数进行切分数组,返回前端指定页码的数据。
demo如下:
mapper层
//数组分页 List<Map<String, Object>> getProductByArrayPage();
sql语句
select * from test
service层方法
@Override public R getProductByArrayPage(int page, int limit) { List<Map<String,Object>> data = productMapper.getProductByArrayPage(); //从第几条开始 int startNum = (page-1)*limit; //到第几条结束 int lastNum = page * limit; if (lastNum>data.size()){ lastNum = data.size(); } return R.success().set("count",data.size()).data(data.subList(startNum,lastNum)); }
//统一返回格式(根据自己业务来定) public class R extends HashMap{ public static String SUCCESS_CODE="200"; public static String ERROR_CODE="500"; public static String DATA_KEY = "data"; public static String MSG_KEY = "msg"; private R(){ } public R set(String key, Object object){ super.put(key,object); return this; } private static R ok(){ return new R(); } public static R success(){ return R.ok().set("code", R.SUCCESS_CODE).set(R.MSG_KEY,"操作成功"); } public static R success(String msg){ return R.ok().set("code", R.SUCCESS_CODE).set(R.MSG_KEY,msg); } public static R success(String msg, Object object){ return R.ok().set("code", R.SUCCESS_CODE).set(R.MSG_KEY,msg).set(R.DATA_KEY,object); } public R data(Object obj){ return this.set("data",obj); } public static R error(){ return R.ok().set(R.MSG_KEY,"操作失败").set("code", R.ERROR_CODE); } public static R error(String msg){ return R.ok().set(R.MSG_KEY,msg).set("code", R.ERROR_CODE); } public static R error(String msg, Object object){ return R.ok().set(R.MSG_KEY,msg).set(R.DATA_KEY,object).set("code", R.ERROR_CODE); } }
2.数据库分页
思路: 手动写一个查询集合的sql,一个获取总数的接口,属于物理分页。
- sql层
<select id="getProductPage" parameterType="Map" resultType="Map"> select * from test limit #{start} , #{limit} </select> <select id="getProductCount" resultType="int"> select count(0) from test </select>
- mapper层
//数据库分页 List<Map<String, Object>> getProductPage(Map<String, Object> map); //获取总数 int getProductCount();
- service 层
public R getProductPage(int page,int limit) { Map<String,Object> map = new HashMap<>(); map.put("start",(page-1)*limit); map.put("limit",limit); System.out.println("调用了sql分页"); return R.success().data(productMapper.getProductPage(map)).set("count",productMapper.getProductCount()); }
3.Rowbounds分页
Mybatis可以通过传递RowBounds对象,来进行数据库数据的分页操作,该分页操作是对ResultSet结果集进行分页,也就是人们常说的逻辑分页,而非物理分页
。
demo如下:
sql:
<select id="getProductByPage" parameterType="Map" resultType="Map"> select * from test </select> <select id="getProductCount" resultType="int"> select count(0) from test </select>
mapper层
//Rowbounds分页 List<Map<String, Object>> getProductByRowboundsPage(RowBounds rowBounds); //获取总数 int getProductCount();
- service 层
public R getProductByRowboundsPage(int page,int limit) { RowBounds rowBounds = new RowBounds((page-1)*limit,limit); System.out.println("调用了RowBounds分页"); return R.success().data(productMapper.getProductByRowboundsPage(rowBounds)).set("count",productMapper.getProductCount()); }
4.自定义插件分页
自定义分页原理
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (
update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
) - ParameterHandler (
getParameterObject, setParameters
) - ResultSetHandler (
handleResultSets, handleOutputParameters
) - StatementHandler (
prepare, parameterize, batch, update, query
)
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
Interceptor 如下图。
分页无非就是获取总数和获取每页的数据,我们自定义实现拦截器,在mybatis拼接sql时,自动为我们计算count记录数,然后执行分页查询即可。
自定义分页实战
demo如下:
- 自定义分页插件(拦截器)
//args : 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法 @Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class})) public class MyPagePlugin implements Interceptor { String databaseType = ""; String pageSqlId = ""; public String getDatabaseType() { return databaseType; } public void setDatabaseType(String databaseType) { this.databaseType = databaseType; } public String getPageSqlId() { return pageSqlId; } public void setPageSqlId(String pageSqlId) { this.pageSqlId = pageSqlId; } //我们自己拦截器里面的逻辑 @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // statementHandler.getDelegate().getmappedStatement().getId(); // Field delegate = StatementHandler.class.getDeclaredField("delegate"); // delegate.setAccessible(true); // Object o = delegate.get(statementHandler); // o.getClass().getDeclaredField("mappedStatement"). MetaObject metaObject = MetaObject.forObject( statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory()); String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id"); //判断一下是否是分页 // <!--第一步 执行一条couunt语句--> //1.1拿到连接 //1.2 预编译SQL语句 拿到绑定的sql语句 //1.3 执行 count语句 怎么返回你执行count的结果 // <!--第二部 重写sql select * from luban_product limit start,limit --> //2.1 ? 怎么知道 start 和limit //2.2拼接start 和limit //2.3 替换原来绑定sql //拿到原来应该执行的sql if (sqlId.matches(pageSqlId)){ ParameterHandler parameterHandler = statementHandler.getParameterHandler(); //原来应该执行的sql String sql = statementHandler.getBoundSql().getSql(); //sql= select * from product select count(0) from (select * from product) as a //select * from luban_product where name = #{name} //执行一条count语句 //拿到数据库连接对象 Connection connection = (Connection) invocation.getArgs()[0]; String countSql = "select count(0) from ("+sql+") a"; System.out.println(countSql); //渲染参数 PreparedStatement preparedStatement = connection.prepareStatement(countSql); //条件交给mybatis parameterHandler.setParameters(preparedStatement); ResultSet resultSet = preparedStatement.executeQuery(); int count =0; if (resultSet.next()) { count = resultSet.getInt(1); } resultSet.close(); preparedStatement.close(); //获得你传进来的参数对象 Map<String, Object> parameterObject = (Map<String, Object>) parameterHandler.getParameterObject(); //limit page PageUtil pageUtil = (PageUtil) parameterObject.get("page"); //limit 1 ,10 十条数据 总共可能有100 count 要的是 后面的100 pageUtil.setCount(count); //拼接分页语句(limit) 并且修改mysql本该执行的语句 String pageSql = getPageSql(sql, pageUtil); metaObject.setValue("delegate.boundSql.sql",pageSql); System.out.println(pageSql); } //推进拦截器调用链 return invocation.proceed(); } public String getPageSql(String sql,PageUtil pageUtil){ if(databaseType.equals("mysql")){ return sql+" limit "+pageUtil.getStart()+","+pageUtil.getLimit(); }else if(databaseType.equals("oracle")){ //拼接oracle的分语句 } return sql+" limit "+pageUtil.getStart()+","+pageUtil.getLimit(); } //需要你返回一个动态代理后的对象 target :StatementHandler @Override public Object plugin(Object target) { return Plugin.wrap(target,this); } //会传入配置文件内容 用户可根据配置文件自定义 @Override public void setProperties(Properties properties) { } }
其他代码
sql
<select id="getProductByPage" parameterType="Map" resultType="Map"> select * from test </select>
mapper
//插件分页 List<Map<String, Object>> getProductByPage(Map<String, Object> map);
service层就很简单了。
自定义个分页对象
@Data public class PageUtil { private int page; private int limit; private int count; private int start;
//分页的话 入参对象中添加page对象属性。
@Override public R getProductByPage(int page,int limit) { Map<String,Object> map =new HashMap<>(); //只要在参数中 添加page属性就自动我们分页了 PageUtil pageUtil = new PageUtil(page,limit); map.put("page",pageUtil); return R.success().data(productMapper.getProductByPage(map)).set("count",pageUtil.getCount()); }
聊下第三方分页插件
其他的第三方插件页无非就是和我们自定义插件一样的原理,只是做了其他优化,比如放在一个ThreadLocal中包证线程安全,对sql进行优化等。
pageHelper分页插件
这个分页插件功能代码很多,我们抓主干只看关键的地方:
- 让我们封装进什么对象作为参数传入接口
- 分页拦截器里面哪里执行了分页控制,即是否要进行分页
- 分页拦截器里面在哪里给我们写了查询总数count的接口,在哪里执行了分页查询
- 返回时包装成PageInfo统一对象为我们做了什么。
使用上就是使用pageHelper.startPage(…)这个方法。
我们看下他的实现原理:
- 1.pageHelper.startPage(…)
继续跟
这里面会获取到一个Page对象,放在ThreadLocal对象中,后面会根据是否有page对象进行是否进行分页:
- 拦截器
很熟悉,就是我们之前自定义分页拦截器一样的原理。
也是实现了mybatis的拦截器进行处理sql
获取总数的方法 - 在拦截器里面看下怎么过滤需不需要分页的?
没有分页的话,我们不会调用PageHelper.startPage
方法,也就从ThreadLocal LOCAL_PAGE这个属性中取不出page,就不分页。
如果不分页就走自己的不分页的sql: - 返回时包装成PageInfo统一对象为我们做了什么。
返回后page只给我们赋值了记录数。所以需要我们手动把处理后的数据,放在返回pageInfo对象中:
显示调用
new pageInfo的逻辑其实就是:PageInfo因为继承了PageSerializable,所以含有total和list2个属性,给与其赋值。
这样pagehelper的分页就简单梳理完了。
再来聊聊另一个分页插件。
mybats-plus的分页插件
使用
mybats-plus的分页插件的使用先很简单
首先引入其分页插件:
使用mybatis-plus分页插件
在mapper中传入Ipage对象
然后在sql中写我们的分页sql就可以了。
分析
个人觉得mybatis-plus分页很清晰,不是很绕。
这里显示在StatementHandler拼接sql的时候可以做些事情:
对StatementHandler的prepare方法处进行拦截添加业务逻辑
拦截方法: 判断是否需要分页
分页操作:
查询记录数
拼装分页语句获取集合
拼接分页数据
获取总记录数赋值给page对象
最后还是需要我们自己把集合对象赋值给Ipage对象
以上就是mybatis的分页个人理解,并实现了简单的粗糙的自定义分页插件封装,对第三方的分页插件进行浅浅的分析。
完