Mybatis拦截器运用-物理分页

来源:互联网 发布:阿尔法橱柜门板软件 编辑:程序博客网 时间:2024/05/21 04:41
    mybatis是一个优秀的ORM产品,但是在我们的实际代码中需要对数据分页查询的时候,mybatis就比较弱了,只提供了逻辑内存分页,这种鸡肋的分页功能可能不能用,所以我们就自己来写个分页。注意:以下代码只针对使用mysql数据。
自己实现分页功能整个逻辑以及代码过程如下:
1、定义一个分页对象存储分页相关信息(PageInfo.java)
public class PageInfo { private int pageNo = 1;// 当前页编号 private int dbIndex = 0;// 起始行,通常该属性通过pageNo和pageSize计算得到 private int pageSize = 5;// 每页显示条数 private int totalReacordNumber;// 总共的记录条数 private int totalPageNumber;// 总共的页数,通过总共的记录条数以及每页大小计算而得 public int getPageNo() {  return pageNo; } public void setPageNo(int pageNo) {  //在设置当前页码的时候,就同时设置分页起始行  int temp = (pageNo - 1) < 0 ? 0 : (pageNo - 1);  this.dbIndex = temp * pageSize;  this.pageNo = pageNo; } public int getPageSize() {  return pageSize; } public void setPageSize(int pageSize) {  this.pageSize = pageSize; } public int getDbIndex() {  return dbIndex; } public void setDbIndex(int dbIndex) {  this.dbIndex = dbIndex; } public int getTotalReacordNumber() {  return totalReacordNumber; } public void setTotalReacordNumber(int totalReacordNumber) {  if (totalReacordNumber % pageSize == 0) {   this.totalPageNumber = totalReacordNumber / pageSize;  } else {   this.totalPageNumber = totalReacordNumber / pageSize + 1;  }  this.totalReacordNumber = totalReacordNumber; } public int getTotalPageNumber() {  return totalPageNumber; } public void setTotalPageNumber(int totalPageNumber) {  this.totalPageNumber = totalPageNumber; } @Override public String toString() {  return "PageInfo [currentPageNo=" + pageNo + ", dbIndex=" + dbIndex + ", dbNumber=" + pageSize + ", totalReacordNumber="    + totalReacordNumber + ", totalPageNumber=" + totalPageNumber + "]"; }}
2、定义实体对象User(User.java)
public class User { private int id; private String username; private String password; //setter(),getter()}
3、完成实体对象User的映射文件(User.xml)
<mapper namespace="com.gusi.demo.idao.IUser"> <resultMap type="com.gusi.demo.pojo.User" id="UserResult">  <id column="id" jdbcType="INTEGER" property="id" />  <result column="username" jdbcType="VARCHAR" property="username" />  <result column="password" jdbcType="VARCHAR" property="password" /> </resultMap><!--计算符合查询条件的结果总数--> <select id="count" parameterType="UserAlias" resultType="int">  select count(*) from user  <where>   <!-- xml中and符号等需要使用特定字符串表示,还有${}标签和#{}标签的区别 -->   <if test="username !=null && username.length() > 0">and username like '%${username}%'</if>  </where> </select> <!-- 注意通过map接收参数,map中既有查询参数,也有分页参数 -->  <select id="queryListUser" parameterType="java.util.Map"  resultMap="UserResult">  select * from user  <where>   <if test="user.username !=null && user.username.length() > 0">username like '%${user.username}%'</if>  </where>  limit #{pageInfo.dbIndex} , #{pageInfo.pageSize}<!--这里配置分页查询,注意只是针对mysql数据库--> </select>  </mapper>
4、编写查询数据dao类(UserDao.java)
public class UserDao { MybatisUtils mybatisUtils = new MybatisUtils(); SqlSession sqlSession = null; // 通过手动写分页统计 public List<User> queryListUser(User queryUser, PageInfo pageInfo) {  List<User> userList = null;  try {   sqlSession = mybatisUtils.getSqlSession();  } catch (IOException e) {   e.printStackTrace();  }//通过面向接口编程访问  IUser iUser = sqlSession.getMapper(IUser.class);//设置请求参数,加入到map中  Map<String, Object> parameter = new HashMap<String, Object>();  parameter.put("user", queryUser);//提前查询结果总数,放到pageInfo中  int count = iUser.count(queryUser);  pageInfo.setTotalReacordNumber(count);//注意在给pageInfo设置总数的时候会设置总页数  parameter.put("pageInfo", pageInfo);//查询结果  userList = iUser.queryListUser(parameter);  return userList; }}
5、测试
UserDao dao = new UserDao();User queryUser = new User();queryUser.setUsername("a");PageInfo pageInfo = new PageInfo();pageInfo.setPageSize(2);pageInfo.setPageNo(1);List<User> userList = dao.queryListUser(queryUser, pageInfo);System.out.println(pageInfo);for (User user : userList) { System.out.println(user);}
结果:
PageInfo [pageNo=1, dbIndex=0, pageSize=2, totalReacordNumber=4, totalPageNumber=2]
User [id=1, username=aaa, password=aaa]
User [id=2, username=abc, password=abc]

    对于上面这样的结果完全符合我们的要求,返回了两条结果数据,同时返回了分页信息以供客户端显示。但是这样做是不优雅的,我们希望能一劳永逸,不用每次写一个查询就要写对应的分页,太麻烦,那么我们就使用mybatis提供的拦截器写一个自动拦截sql语句,然后将分页信息加上然后再返回给我们的东西。
说在前面:
a.拦截器目的:拦截mybatis执行原始sql语句,将sql修改为带分页的然后再让mybatis执行
b.拦截位置:在mybatis执行处理sql语句的时候,其实就是在StatementHandler类中。
c.类StatementHandler的继承关系入如下:

mybatis处理的流程是得到一个RoutingStatementHandler对象,该对象中有个属性是StatementHandler类型的delegate,和数据库交互的信息都在BaseStatementHanler的一个对象中,包括连接信息,sql信息以及参数等等
d.源码中几个关键类的重要属性如下:





e.Mybatis给我们提供了一个工具类MetaObject,该类可以快捷的通过反射获取某个对象的私有或者保护属性以及给属性设置值,该对象有三个重要方法是forObject()获得对象实例、getValue()获得对象的某个属性、setValue()设置对象的某个属性,支持ognl方法的访问属性(通过obj.attr方式访问),当然我们也可以不使用这个工具类,自己通过反射实现我们的需要的东西。

通过拦截器实现分页过程代码逻辑如下:
上面用到PageInfo类以及实体User类还是继续使用
第一步:定义个一个拦截器类(PageInterceptor.java)核心代码
//首先通过注解定义该拦截器的切入点,对那个类的哪个方法进行拦截,防止方法重载需要声明参数类型以及个数@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })public class PageInterceptor implements Interceptor { public String sqlIdByPageRegex = "";// 这则表达式用了筛选所有分页的sql语句 public Object intercept(Invocation invocation) throws Throwable {  // 通过拦截器得到被拦截的对象,就是上面配置的注解的对象  StatementHandler statementHandler = (StatementHandler) invocation.getTarget();  // 为了获取以及设置某些对象的属性值(某些对象的属性是没有getter/setter的),mybatis提供的快捷的通过反射设置获取属性只的工具类,当然也可以通过自己写反射完成  MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,    SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());  // 得到当前的mapper对象信息,即为各种select,update,delete,insert语句的映射配置信息,通过上面的工具类获取属性对象  MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");  // 对映射语句进行选择过滤,如果是以ByPage结尾就拦截,否则不拦截  String sqlId = mappedStatement.getId();  if (sqlId.matches(sqlIdByPageRegex)) {   // sql语句在对象BoundSql对象中,这个对象有get方法可以直接获取   BoundSql boundSql = statementHandler.getBoundSql();   // 获取原始sql,该sql是预处理的,有参数还没有被设置,被问好代替了   String sql = boundSql.getSql();   // 拿到我们给sql传入的参数对象,我们那儿写的Map类型,所以这里就是使用map接收,当然也可以是其他类型   Map<?, ?> parameterMap = (Map<?, ?>) boundSql.getParameterObject();   // 参数对象中的pageInfo对象信息拿到   PageInfo pageInfo = (PageInfo) parameterMap.get("pageInfo");   // 获取总条数,通过自己写sql查询,然后设置给pageinfo对象   String countSql = "select count(*) from (" + sql + ") alias";// 注意这里通过子查询需要给字结果设置别名   // 同jdbc一个流程查询sql语句   Connection connection = (Connection) invocation.getArgs()[0];   PreparedStatement preparedStatement = connection.prepareStatement(countSql);   // 为了先查询总条数,所以需要先统计原始sql结果,但是原始sql中参数还没赋值,所以就需要先拿到原始sql的参数处理对象,通过反射工具   ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");   parameterHandler.setParameters(preparedStatement);   // 参数被设置以后,直接执行sql语句得到结果集合   ResultSet resultSet = preparedStatement.executeQuery();   while (resultSet.next()) {    // 将查询到的结果集合设置到pageInfo中    pageInfo.setTotalReacordNumber(resultSet.getInt(1));   }   // 最后改造分页查询sql   String pageSql = sql + " limit " + pageInfo.getDbIndex() + "," + pageInfo.getPageSize();   // 通过反射将原来的sql给换成加入分页的sql   metaObject.setValue("delegate.boundSql.sql", pageSql);  }  // 连接器是链式结构的,我们完成我们的拦截处理以后,还要保证接下来的其他拦截器或者代码继续执行  return invocation.proceed(); } public Object plugin(Object target) {  // 表示给一个目标对象织入一个拦截器,该代码织入的的拦截器对象就是本身this对象  return Plugin.wrap(target, this); } public void setProperties(Properties properties) {  // 可读取到配置文件中定义的属性以及属性值  sqlIdByPageRegex = (String) properties.get("sqlIdByPageRegex");  System.out.println(sqlIdByPageRegex); }}
整个拦截过程在上面代码中已经说明,这也是自动分页的核心。StatementHandler的默认实现类是RoutingStatementHandler,因此拦截的实际对象是它。RoutingStatementHandler的主要功能是分发,它根据配置Statement类型创建真正执行数据库操作的StatementHandler,并将其保存到delegate属性里。由于delegate是一个私有属性并且没有提供访问它的方法,因此需要借助MetaObject的帮忙。通过MetaObject的封装后我们可以轻易的获得想要的属性,以及给属性设置值。

第二步:注册拦截器(Configuration.xml)
在总的配置文件中声明该拦截器,通过使用 插件的方式,具体代码是在Configuration.xml中加入以下配置
 <plugins>  <!-- 声明一个分页拦截器 -->  <plugin interceptor="com.gusi.demo.interceptor.PageInterceptor">   <!-- 给拦截器传入一个参数(该参数在拦截器中判断拦截到的sql是不是需要分页的使用) -->   <property name="sqlIdByPageRegex" value=".+ByPage$" />  </plugin> </plugins>
第三步:编写映射文件(User.xml)
本次映射文件User.xml中将只写查询信息,并不写分页信息,分页通过拦截器完成,而且还要注意sql语句的Id和我们在拦截器中过滤条件相匹配
<!-- 通过拦截器帮助完成分页查询 --><select id="queryListUserByPage" parameterType="java.util.Map" resultMap="UserResult"> select * from user <where>  <if test="user.username !=null && user.username.length() > 0">username like '%${user.username}%'</if> </where> <!-- limit #{pageInfo.dbIndex} , #{pageInfo.dbNumber} --></select>
第四步:编写接口dao方法(UserDao.java)
// 通过拦截器查询分页public List<User> queryListUserByPage(User queryUser, PageInfo pageInfo) { List<User> userList = null; try {  sqlSession = mybatisUtils.getSqlSession(); } catch (IOException e) {  e.printStackTrace(); } IUser iUser = sqlSession.getMapper(IUser.class); Map<String, Object> parameter = new HashMap<String, Object>(); parameter.put("user", queryUser); //此处不再需要统计总数,在拦截器中自动统计 // int count = iUser.count(queryUser); // pageInfo.setTotalReacordNumber(count); parameter.put("pageInfo", pageInfo); userList = iUser.queryListUserByPage(parameter); return userList;}
第五步:测试
UserDao dao = new UserDao();User queryUser = new User();queryUser.setUsername("a");PageInfo pageInfo = new PageInfo();pageInfo.setPageSize(2);pageInfo.setPageNo(1);List<User> userList = null;pageInfo.setPageNo(2);userList = dao.queryListUserByPage(queryUser, pageInfo);System.out.println(pageInfo);for (User user : userList) { System.out.println(user);}
结果:
PageInfo [pageNo=2, dbIndex=2, pageSize=2, totalReacordNumber=4, totalPageNumber=2]
User [id=4, username=acd, password=acd]
User [id=6, username=abb, password=abb]

对比两种方式的日志:
手动分页:
[DEBUG] "==>  Preparing: select count(*) from user WHERE username like '%a%' " 
[DEBUG] "==> Parameters: " org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
[DEBUG] "<==      Total: 1" org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
[DEBUG] "==>  Preparing: select * from user WHERE username like '%a%' limit ? , ? " 
[DEBUG] "==> Parameters: 0(Integer), 2(Integer)" org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
拦截器分页:
[DEBUG] "==>  Preparing: select count(*) from (select * from user WHERE username like '%a%') alias " 
[DEBUG] "==> Parameters: " org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
[DEBUG] "<==      Total: 1" org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
[DEBUG] "==>  Preparing: select * from user WHERE username like '%a%' limit 2,2 " 
[DEBUG] "==> Parameters: " org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
[DEBUG] "<==      Total: 2" org.apache.ibatis.logging.jdbc.BaseJdbcLogger.debug(BaseJdbcLogger.java:142)
    
    通过日志可以明显看到执行的sql语句其实是不同的。但是效果是相同的。拦截器除了帮我们完成一劳永逸的分页,当然还可以帮我们完成其他很多功能,比如权限控制,日志记录等等。

项目源码:https://github.com/yoting/demoMybatis
1 0