转自 http://blog.csdn.net/hupanfeng/article/details/9265341
Mybatis的分页功能很弱,它是基于内存的分页(查出所有记录再按偏移量和limit取结果),在大数据量的情况下这样的分页基本上是没有用的。本文基于插件,通过拦截StatementHandler重写sql语句,实现数据库的物理分页。本文适配的mybatis版本是3.2.2。
准备
为什么在StatementHandler拦截
在深入浅出MyBatis-Sqlsession章节介绍了一次sqlsession的完整执行过程,从中可以知道sql的解析是在StatementHandler里完成的,所以为了重写sql需要拦截StatementHandler。
MetaObject简介
在我的实现里大量使用了MetaObject这个对象,因此有必要先介绍下它。MetaObject是Mybatis提供的一个的工具类,通过它包装一个对象后可以获取或设置该对象的原本不可访问的属性(比如那些私有属性)。它有个三个重要方法经常用到:
1) MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)
2) Object getValue(String name)
3) void setValue(String name, Object value)
方法1)用于包装对象;方法2)用于获取属性的值(支持OGNL的方法);方法3)用于设置属性的值(支持OGNL的方法);
插件的原理
有了上面这些基础知识的准备后,就可以我们的主题了。
拦截器签名
- @Intercepts({@Signature(type =StatementHandler.class, method = "prepare", args ={Connection.class})})
- publicclass PageInterceptor implementsInterceptor {
- ...
- }
从签名里可以看出,要拦截的目标类型是StatementHandler(注意:type只能配置成接口类型),拦截的方法是名称为prepare参数为Connection类型的方法。
intercept的实现
- public Object intercept(Invocation invocation) throws Throwable {
- StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
- MetaObject metaStatementHandler = MetaObject.forObject(statementHandler,
- DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
- // 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过下面的两次循环
- // 可以分离出最原始的的目标类)
- while (metaStatementHandler.hasGetter("h")) {
- Object object = metaStatementHandler.getValue("h");
- metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY,
- DEFAULT_OBJECT_WRAPPER_FACTORY);
- }
- // 分离最后一个代理对象的目标类
- while (metaStatementHandler.hasGetter("target")) {
- Object object = metaStatementHandler.getValue("target");
- metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY,
- DEFAULT_OBJECT_WRAPPER_FACTORY);
- }
- Configuration configuration = (Configuration) metaStatementHandler.
- getValue("delegate.configuration");
- dialect = configuration.getVariables().getProperty("dialect");
- if (null == dialect || "".equals(dialect)) {
- logger.warn("Property dialect is not setted,use default 'mysql' ");
- dialect = defaultDialect;
- }
- pageSqlId = configuration.getVariables().getProperty("pageSqlId");
- if (null == pageSqlId || "".equals(pageSqlId)) {
- logger.warn("Property pageSqlId is not setted,use default '.*Page$' ");
- pageSqlId = defaultPageSqlId;
- }
- MappedStatement mappedStatement = (MappedStatement)
- metaStatementHandler.getValue("delegate.mappedStatement");
- // 只重写需要分页的sql语句。通过MappedStatement的ID匹配,默认重写以Page结尾的
- // MappedStatement的sql
- if (mappedStatement.getId().matches(pageSqlId)) {
- BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
- Object parameterObject = boundSql.getParameterObject();
- if (parameterObject == null) {
- throw new NullPointerException("parameterObject is null!");
- } else {
- // 分页参数作为参数对象parameterObject的一个属性
- PageParameter page = (PageParameter) metaStatementHandler
- .getValue("delegate.boundSql.parameterObject.page");
- String sql = boundSql.getSql();
- // 重写sql
- String pageSql = buildPageSql(sql, page);
- metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
- // 采用物理分页后,就不需要mybatis的内存分页了,所以重置下面的两个参数
- metaStatementHandler.setValue("delegate.rowBounds.offset",
- RowBounds.NO_ROW_OFFSET);
- metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
- Connection connection = (Connection) invocation.getArgs()[0];
- // 重设分页参数里的总页数等
- setPageParameter(sql, connection, mappedStatement, boundSql, page);
- }
- }
- // 将执行权交给下一个拦截器
- return invocation.proceed();
- }
StatementHandler的默认实现类是RoutingStatementHandler,因此拦截的实际对象是它。RoutingStatementHandler的主要功能是分发,它根据配置Statement类型创建真正执行数据库操作的StatementHandler,并将其保存到delegate属性里。由于delegate是一个私有属性并且没有提供访问它的方法,因此需要借助MetaObject的帮忙。通过MetaObject的封装后我们可以轻易的获得想要的属性。
在上面的方法里有个两个循环,通过他们可以分离出原始的RoutingStatementHandler(而不是代理对象)。
前面提到,签名里配置的要拦截的目标类型是StatementHandler拦截的方法是名称为prepare参数为Connection类型的方法,而这个方法是每次数据库访问都要执行的。因为我是通过重写sql的方式实现分页,为了不影响其他sql(update或不需要分页的query),我采用了通过ID匹配的方式过滤。默认的过滤方式只对id以Page结尾的进行拦截(注意区分大小写),如下:
- <select id="queryUserByPage" parameterType="UserDto" resultType="UserDto">
- <![CDATA[
- select * from t_user t where t.username = #{username}
- ]]>
- </select>
当然,也可以自定义拦截模式,在mybatis的配置文件里加入以下配置项:
- <properties>
- <property name="dialect" value="mysql" />
- <property name="pageSqlId" value=".*Page$" />
- </properties>
其中,属性dialect指示数据库类型,目前只支持mysql和oracle两种数据库。其中,属性pageSqlId指示拦截的规则,以正则方式匹配。
sql重写
sql重写其实在原始的sql语句上加入分页的参数,目前支持mysql和oracle两种数据库的分页。
- private String buildPageSql(String sql, PageParameter page) {
- if (page != null) {
- StringBuilder pageSql = new StringBuilder();
- if ("mysql".equals(dialect)) {
- pageSql = buildPageSqlForMysql(sql, page);
- } else if ("oracle".equals(dialect)) {
- pageSql = buildPageSqlForOracle(sql, page);
- } else {
- return sql;
- }
- return pageSql.toString();
- } else {
- return sql;
- }
- }
mysql的分页实现:
- public StringBuilder buildPageSqlForMysql(String sql, PageParameter page) {
- StringBuilder pageSql = new StringBuilder(100);
- String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
- pageSql.append(sql);
- pageSql.append(" limit " + beginrow + "," + page.getPageSize());
- return pageSql;
- }
oracle的分页实现:
- public StringBuilder buildPageSqlForOracle(String sql, PageParameter page) {
- StringBuilder pageSql = new StringBuilder(100);
- String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
- String endrow = String.valueOf(page.getCurrentPage() * page.getPageSize());
- pageSql.append("select * from ( select temp.*, rownum row_id from ( ");
- pageSql.append(sql);
- pageSql.append(" ) temp where rownum <= ").append(endrow);
- pageSql.append(") where row_id > ").append(beginrow);
- return pageSql;
- }
分页参数重写
有时候会有这种需求,就是不但要查出指定页的结果,还需要知道总的记录数和页数。我通过重写分页参数的方式提供了一种解决方案:
- /**
- * 从数据库里查询总的记录数并计算总页数,回写进分页参数<code>PageParameter</code>,这样调用
- * 者就可用通过 分页参数<code>PageParameter</code>获得相关信息。
- *
- * @param sql
- * @param connection
- * @param mappedStatement
- * @param boundSql
- * @param page
- */
- private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
- BoundSql boundSql, PageParameter page) {
- // 记录总记录数
- String countSql = "select count(0) from (" + sql + ") as total";
- PreparedStatement countStmt = null;
- ResultSet rs = null;
- try {
- countStmt = connection.prepareStatement(countSql);
- BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
- boundSql.getParameterMappings(), boundSql.getParameterObject());
- setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
- rs = countStmt.executeQuery();
- int totalCount = 0;
- if (rs.next()) {
- totalCount = rs.getInt(1);
- }
- page.setTotalCount(totalCount);
- int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
- page.setTotalPage(totalPage);
- } catch (SQLException e) {
- logger.error("Ignore this exception", e);
- } finally {
- try {
- rs.close();
- } catch (SQLException e) {
- logger.error("Ignore this exception", e);
- }
- try {
- countStmt.close();
- } catch (SQLException e) {
- logger.error("Ignore this exception", e);
- }
- }
- }
- /**
- * 对SQL参数(?)设值
- *
- * @param ps
- * @param mappedStatement
- * @param boundSql
- * @param parameterObject
- * @throws SQLException
- */
- private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
- Object parameterObject) throws SQLException {
- ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
- parameterHandler.setParameters(ps);
- }
plugin的实现
- public Object plugin(Object target) {
- // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身,减少目标被代理的
- // 次数
- if (target instanceof StatementHandler) {
- return Plugin.wrap(target, this);
- } else {
- return target;
- }
- }
相关推荐
《深入浅出MyBatis技术原理与实战》是一本针对Java开发者深度解析MyBatis框架的专业书籍。MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射,旨在简化Java开发中的数据库操作。这本书通过高清...
这本书《深入浅出MyBatis技术原理与实战》由杨开振编著,旨在帮助初学者系统地理解和掌握MyBatis,提升到精通的层次。 首先,MyBatis的核心概念包括XML配置文件和注解方式。XML配置文件是MyBatis的传统方式,用于...
《深入浅出Mybatis技术原理与实战》这本书是针对Mybatis这一流行持久层框架的一份详尽指南。Mybatis是一个优秀的Java ORM(对象关系映射)框架,它允许开发者将数据库操作与应用程序逻辑分离,提高了代码的可读性...
MyBatis提供插件扩展能力,可以通过拦截器实现SQL执行前后的增强,如PageHelper分页插件、LogInterceptor日志插件等。 八、事务管理 MyBatis支持手动和自动两种事务管理方式。在手动模式下,开发者需要自行调用...
《深入浅出MyBatis技术原理与实战》一书,旨在为Java开发人员提供一份全面且深入的MyBatis框架实战教程。MyBatis作为一款轻量级的Java持久层框架,深受广大开发者喜爱,其核心特性在于将SQL语句与Java代码紧密集成,...
《高清深入浅出MyBatis技术原理与实战》是一本深度剖析MyBatis框架的书籍,旨在帮助读者全面理解MyBatis的使用方法、优化策略以及底层架构。这本书覆盖了从基础到高级的各种主题,适合不同层次的开发人员学习。 ...
《深入浅出MyBatis技术原理与实践》是一本专注于解析MyBatis这一流行Java持久层框架的专业书籍。MyBatis作为一个轻量级的ORM(对象关系映射)框架,它将SQL语句与Java代码分离,使得开发更加灵活且易于维护。这本书...
《MyBatis 深入浅出》是一本旨在引导初学者从入门到精通的教程,专注于MyBatis这一流行的Java持久层框架。MyBatis是一个优秀的轻量级框架,它解决了传统JDBC中的繁琐代码问题,使得数据库操作更加简便易行。本教程将...
总之,《3-2_MyBatis持久层框架.pdf》深入浅出地介绍了MyBatis的使用方法,从基础操作到高级特性,为读者提供了全面的学习资源。通过学习,开发者能够熟练掌握MyBatis框架,提升数据库操作的效率和代码质量。
提到“老师讲的很好”,这表明视频质量高,讲解深入浅出,适合初学者和有一定基础的学习者。 【标签】"super"可能表示这些视频教程是精华内容,或者是高级进阶课程,涵盖了MyBatis的高级特性和最佳实践。这可能包括...
《深入浅出SpringBoot实战——基于javaStudy.rar的解析》 SpringBoot作为Java生态中的热门框架,因其简化配置、快速启动以及内置Tomcat等特性,深受开发者喜爱。本篇文章将围绕"javaStudy.rar"这个项目,详细解读...
- **基础概念讲解**:深入浅出地解释Spring Boot、Spring Cloud Alibaba的基本原理和使用方法。 - **实战项目演示**:通过具体的项目案例,手把手指导如何使用若依框架构建完整的微服务应用。 - **高级特性探索**:...
《iBATIS 电子书》是一本专注于介绍iBATIS框架的专业读物,它深入浅出地阐述了iBATIS在Java开发中的应用与实践。iBATIS是阿里巴巴开源的一个持久层框架,它允许开发者将SQL语句直接写在XML配置文件中,实现了SQL与...
├─面试必问-微服务架构深入浅出讲解springcloud │ 微服务架构 --深入浅出讲解springcloud.mp4 │ ├─面试必问-教你手写MyBatis框架 │ 一小时教你手写MyBatis框架.mp4 │ ├─面试必问-架构杀手锏——java混乱...
《Spring开发参考手册》是学习Spring框架不可或缺的重要资源,它以深入浅出的方式全面介绍了Spring的核心概念和技术。Spring作为Java企业级应用开发的主流框架,以其依赖注入(Dependency Injection,DI)和面向切面...
《程序员的SQL金典》是一本专为程序员打造的、深入浅出的SQL学习宝典。这本书涵盖了SQL语言的基础知识,高级查询技巧,以及在实际开发中的应用策略,旨在帮助程序员提升数据库操作技能,提高工作效率。 一、SQL基础...
《Spring in Action》是Spring框架领域的一本经典著作,它以英文版的形式提供,深入浅出地介绍了Spring框架的核心概念和技术。Spring是一个广泛使用的Java企业级应用开发框架,它简化了应用程序的构建,并且提供了...