还记得上一次写的分页吗?
在MyBatis中实现分页的方法是这样的:
1.自定义一个拦截器
2.在拦截器中,根据不同的数据库返回不同的分页SQL语句
3.在配置文件中配置该拦截器插件
大概就是这么个顺序,在上一次实现时,使用了RowBounds,功能可能不是很强大,后来又在网上找到了一个实现方法,现分享一下:
1.pom.xml
这个和上一次是一样的
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>org.ygy.demo</groupId> <artifactId>mybatis-parent</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>mybatis-parent</name> <url>http://maven.apache.org</url> <dependencyManagement> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.1.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>0.12.0</version> </dependency> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc14</artifactId> <version>10.2.0.4.0</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc14</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <modules> <module>mybatis-hello</module> <module>mybatis-relevance</module> <module>mybatis-common</module> <module>mybatis-page</module> <module>mybatis-batch</module> </modules> </project>
这个自定义了一个分页实体,实现了更多的功能
package org.ygy.demo.mybatis.interceptor; import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.Data; /** * 对分页的基本数据进行一个简单的封装 */ @Data public class Page<T> { private int start = 1;// 页码,默认是第一页 private int pageSize = 15;// 每页显示的记录数,默认是15 private int totalRecord;// 总记录数 private int totalPage;// 总页数 private List<T> results;// 对应的当前页记录 private Map<String, Object> params = new HashMap<String, Object>();// 其他的参数我们把它分装成一个Map对象 public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; // 在设置总条数的时候计算出对应的总页数,在下面的三目运算中加法拥有更高的优先级,所以最后可以不加括号。 int totalPage = totalRecord % pageSize == 0 ? totalRecord / pageSize : totalRecord / pageSize + 1; this.setTotalPage(totalPage); } }
这个拦截器的实现,原作者写了很多的注释,很有用,感谢分享(ps:很抱歉,忘记在哪里找的了)
package org.ygy.demo.mybatis.interceptor; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Properties; import org.apache.ibatis.executor.parameter.DefaultParameterHandler; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; /** * * 分页拦截器,用于拦截需要进行分页查询的操作,然后对其进行分页处理。 利用拦截器实现Mybatis分页的原理: * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象 * ,Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象,而且对应的Sql语句 * 是在Statement之前产生的,所以我们就可以在它生成Statement之前对用来生成Statement的Sql语句下手 * 。在Mybatis中Statement语句是通过RoutingStatementHandler对象的 * prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法 * ,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用 * StatementHandler对象的prepare方法,即调用invocation.proceed()。 * 对于分页而言,在拦截器里面我们还需要做的一个操作就是统计满足当前条件的记录一共有多少 * ,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设 * 置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。 * */ @Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) }) public class PageInterceptor implements Interceptor { private String databaseType;// 数据库类型,不同的数据库有不同的分页方法 /** * 拦截后要执行的方法 */ public Object intercept(Invocation invocation) throws Throwable { // 对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另一个是抽象类BaseStatementHandler, // BaseStatementHandler有三个子类,分别是SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler, // SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是 // 处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个 // StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、 // PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。 // 我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候 // 是通过Interceptor的plugin方法进行包裹的,所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。 RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget(); // 通过反射获取到当前RoutingStatementHandler对象的delegate属性 StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate"); // 获取到当前StatementHandler的 // boundSql,这里不管是调用handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的,因为之前已经说过了 // RoutingStatementHandler实现的所有StatementHandler接口方法里面都是调用的delegate对应的方法。 BoundSql boundSql = delegate.getBoundSql(); // 拿到当前绑定Sql的参数对象,就是我们在调用对应的Mapper映射语句时所传入的参数对象 Object obj = boundSql.getParameterObject(); // 这里我们简单的通过传入的是Page对象就认定它是需要进行分页操作的。 if (obj instanceof Page<?>) { Page<?> page = (Page<?>) obj; // 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性 MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement"); // 拦截到的prepare方法参数是一个Connection对象 Connection connection = (Connection) invocation.getArgs()[0]; // 获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句 String sql = boundSql.getSql(); // 给当前的page参数对象设置总记录数 this.setTotalRecord(page, mappedStatement, connection); // 获取分页Sql语句 String pageSql = this.getPageSql(page, sql); // 利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句 ReflectUtil.setFieldValue(boundSql, "sql", pageSql); } return invocation.proceed(); } /** * 拦截器对应的封装原始对象的方法 */ public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 设置注册拦截器时设定的属性 * 该方法,会在配置文件加载前执行 */ public void setProperties(Properties properties) { this.databaseType = properties.getProperty("databaseType") == null ? "oracle" : properties.getProperty("databaseType"); } /** * 根据page对象获取对应的分页查询Sql语句,这里只做了两种数据库类型,Mysql和Oracle 其它的数据库都 没有进行分页 * * @param page * 分页对象 * @param sql * 原sql语句 * @return */ private String getPageSql(Page<?> page, String sql) { StringBuffer sqlBuffer = new StringBuffer(sql); if ("mysql".equalsIgnoreCase(databaseType)) { return getMysqlPageSql(page, sqlBuffer); } else if ("oracle".equalsIgnoreCase(databaseType)) { return getOraclePageSql(page, sqlBuffer); } return sqlBuffer.toString(); } /** * 获取Mysql数据库的分页查询语句 * * @param page * 分页对象 * @param sqlBuffer * 包含原sql语句的StringBuffer对象 * @return Mysql数据库分页语句 */ private String getMysqlPageSql(Page<?> page, StringBuffer sqlBuffer) { // 计算第一条记录的位置,Mysql中记录的位置是从0开始的。 int offset = (page.getStart() - 1) * page.getPageSize(); sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize()); return sqlBuffer.toString(); } /** * 获取Oracle数据库的分页查询语句 * * @param page * 分页对象 * @param sqlBuffer * 包含原sql语句的StringBuffer对象 * @return Oracle数据库的分页查询语句 */ private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) { // 计算第一条记录的位置,Oracle分页是通过rownum进行的,而rownum是从1开始的 int offset = (page.getStart() - 1) * page.getPageSize() + 1; sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ") .append(offset + page.getPageSize()); sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset); // 上面的Sql语句拼接之后大概是这个样子: // select * from (select u.*, rownum r from (select * from t_user) u // where rownum < 31) where r >= 16 return sqlBuffer.toString(); } /** * 给当前的参数对象page设置总记录数 * * @param page * Mapper映射语句对应的参数对象 * @param mappedStatement * Mapper映射语句 * @param connection * 当前的数据库连接 */ private void setTotalRecord(Page<?> page, MappedStatement mappedStatement, Connection connection) { // 获取对应的BoundSql,这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。 // delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。 BoundSql boundSql = mappedStatement.getBoundSql(page); // 获取到我们自己写在Mapper映射语句中对应的Sql语句 String sql = boundSql.getSql(); // 通过查询Sql语句获取到对应的计算总记录数的sql语句 String countSql = this.getCountSql(sql); // 通过BoundSql获取对应的参数映射 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); // 利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。 BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page); // 通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象 ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql); // 通过connection建立一个countSql对应的PreparedStatement对象。 PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = connection.prepareStatement(countSql); // 通过parameterHandler给PreparedStatement对象设置参数 parameterHandler.setParameters(pstmt); // 之后就是执行获取总记录数的Sql语句和获取结果了。 rs = pstmt.executeQuery(); if (rs.next()) { int totalRecord = rs.getInt(1); // 给当前的参数page对象设置总记录数 page.setTotalRecord(totalRecord); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 根据原Sql语句获取对应的查询总记录数的Sql语句 * * @param sql * @return */ private String getCountSql(String sql) { int index = sql.toUpperCase().indexOf("FROM"); return "select count(*) " + sql.substring(index); } /** * 利用反射进行操作的一个工具类 * */ private static class ReflectUtil { /** * 利用反射获取指定对象的指定属性 * * @param obj * 目标对象 * @param fieldName * 目标属性 * @return 目标属性的值 */ public static Object getFieldValue(Object obj, String fieldName) { Object result = null; Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { field.setAccessible(true); try { result = field.get(obj); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return result; } /** * 利用反射获取指定对象里面的指定属性 * * @param obj * 目标对象 * @param fieldName * 目标属性 * @return 目标字段 */ private static Field getField(Object obj, String fieldName) { Field field = null; for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) { try { field = clazz.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { // 这里不用做处理,子类没有该字段可能对应的父类有,都没有就返回null。 } } return field; } /** * 利用反射设置指定对象的指定属性为指定的值 * * @param obj * 目标对象 * @param fieldName * 目标属性 * @param fieldValue * 目标值 */ public static void setFieldValue(Object obj, String fieldName, String fieldValue) { Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { try { field.setAccessible(true); field.set(obj, fieldValue); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 配置数据库分页的实现 --> <properties> <property name="dialect" value="oracle" /> <property name="databaseType" value="oracle"></property> </properties> <!-- 全局别名设置,在映射文件中只需写别名,而不必写出整个类路径 --> <typeAliases> <typeAlias type="org.ygy.demo.mybatis.entity.PersonEntity" alias="PersonEntity"></typeAlias> <typeAlias type="org.ygy.demo.mybatis.entity.BlogEntity" alias="BlogEntity"></typeAlias> </typeAliases> <!-- 配置插件 --> <plugins> <!-- 配置拦截器插件 --> <!-- <plugin interceptor="org.ygy.demo.mybatis.page.dialect.PageInterceptor"></plugin> <plugin interceptor="org.ygy.demo.mybatis.interceptor.SimpleInterceptor"></plugin> --> <plugin interceptor="org.ygy.demo.mybatis.interceptor.PageInterceptor"></plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="oracle.jdbc.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@192.168.17.254:1521:dpweb" /> <property name="username" value="dptest" /> <property name="password" value="dptest" /> </dataSource> </environment> </environments> <!-- 配置映射文件地址 --> <mappers> <mapper resource="org/ygy/demo/mybatis/mapper/blog-mapper.xml"></mapper> </mappers> </configuration>
# Global logging configuration log4j.rootLogger=DEBUG, stdout # MyBatis logging configuration... log4j.logger.org.ygy.demo.mybatis=DEBUG # Console output... log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n #log4j.logger.com.abc.mapper=DEBUG
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.ygy.demo.mybatis.dao.BlogDao"> <resultMap type="BlogEntity" id="blogMap"> <result column="blogId" property="id"/> <result column="blogTitle" property="title"></result> <result column="blogContent" property="content"></result> <!-- 即has-a的关系 --> <association property="owner" javaType="PersonEntity"> <result column="blogOwnerId" property="id"/> <result column="blogOwnerName" property="name"></result> </association> </resultMap> <select id="queryAll" resultMap="blogMap"> SELECT BLOG.ID blogId , BLOG.TITLE blogTitle , BLOG.CONTENT blogContent , PERSON.ID blogOwnerId , PERSON.NAME blogOwnerName FROM T_YGY_DEMO_BLOG BLOG INNER JOIN T_YGY_DEMO_PERSON PERSON ON BLOG.OWNERID = PERSON.ID </select> <insert id="add" parameterType="BlogEntity"> INSERT INTO T_YGY_DEMO_BLOG(ID , TITLE , CONTENT , OWNERID) VALUES(#{id} , #{title} , #{content} , #{owner.id}) </insert> <select id="queryWithPage" parameterType="org.apache.ibatis.session.RowBounds" resultMap="blogMap"> SELECT BLOG.ID blogId , BLOG.TITLE blogTitle , BLOG.CONTENT blogContent , PERSON.ID blogOwnerId , PERSON.NAME blogOwnerName FROM T_YGY_DEMO_BLOG BLOG INNER JOIN T_YGY_DEMO_PERSON PERSON ON BLOG.OWNERID = PERSON.ID ORDER BY BLOG.ID </select> <select id="queryWithPage2" parameterType="org.ygy.demo.mybatis.interceptor.Page" resultMap="blogMap"> SELECT BLOG.ID blogId , BLOG.TITLE blogTitle , BLOG.CONTENT blogContent , PERSON.ID blogOwnerId , PERSON.NAME blogOwnerName FROM T_YGY_DEMO_BLOG BLOG INNER JOIN T_YGY_DEMO_PERSON PERSON ON BLOG.OWNERID = PERSON.ID ORDER BY BLOG.ID </select> </mapper>
package org.ygy.demo.mybatis.dao.impl; import java.util.List; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.session.SqlSession; import org.ygy.demo.mybatis.common.util.MyBatisUtil; import org.ygy.demo.mybatis.dao.IBlogDao; import org.ygy.demo.mybatis.entity.BlogEntity; import org.ygy.demo.mybatis.interceptor.Page; public class BlogDao implements IBlogDao { private static final String NAMESPACE = "org.ygy.demo.mybatis.dao.BlogDao"; @Override public int add(BlogEntity blog) { SqlSession session = MyBatisUtil.getSession(); int affectCount = session.insert(NAMESPACE + ".add" , blog); session.commit(); session.close(); return affectCount; } @Override public List<BlogEntity> queryAll() { SqlSession session = MyBatisUtil.getSession(); List<BlogEntity> blogs = session.selectList(NAMESPACE + ".queryAll"); session.close(); return blogs; } @Override public List<BlogEntity> queryWithPage(RowBounds rowBounds) { SqlSession session = MyBatisUtil.getSession(); List<BlogEntity> blogs = session.selectList(NAMESPACE + ".queryWithPage" , null , rowBounds); session.commit(); session.close(); return blogs; } @Override public List<BlogEntity> queryAll(Page<BlogEntity> page) { SqlSession session = MyBatisUtil.getSession(); List<BlogEntity> blogs = session.selectList(NAMESPACE + ".queryWithPage2" , page); session.close(); return blogs; } }
@Test public void testQueryWithPage2() { Page<BlogEntity> page = new Page<BlogEntity>(); page.setStart(1); page.setPageSize(1); List<BlogEntity> blogs = blogDao.queryAll(page); for(BlogEntity blog : blogs) { System.out.println("--blog:" + blog); } System.out.println("totalPage->" + page.getTotalPage()); System.out.println("totalRecord->" + page.getTotalRecord()); }