美文网首页Java分布式架构
MyBatis:实现简单物理分页(Plugin的使用)

MyBatis:实现简单物理分页(Plugin的使用)

作者: 消失er | 来源:发表于2018-07-24 11:31 被阅读16次

    MyBatis中的SqlSession接口中提供的分页功能的方法

    // 获取sqlSession的步骤略,statement略,mapper中的映射语句为
    // select * from users 
    List<User> list = sqlSession.selectList(statement, null, new RowBounds(0,3));
    

    MyBatis内置的分页处理器,是通过内存进行分页,结合上面的例子就是MyBatis首先执行select * from users,然后获取结果集ResultSet,接着通过传入的RowBounds中的offset和limit属性来对ResultSet进行加工。如果记录量大的话,这种效率无疑是相当低的。想证实上面这个结论,可以查看MyBatis中的DefaultResultSetHandler类。

    我们可以使用mybatis 提供的plugin,实现sql执行前的拦截;在执行sql查询列表前,装配指定的分页select * from users limit #{offset},#{limit}

    MyBATIS plugin初始化&原理

    MyBATIS是在初始化上下文环境的时候就初始化插件的,mybatis 的plugin实质上就是拦截器。

    MyBatis Plugin的实现采用了Java的动态代理,应用了责任链设计模式。可以在mybatis-config.xml中加入多个plugin,也就是可以加入多个<plugin>节点,多个拦截器采用链式执行。

    <plugins>
            <plugin interceptor="com.ljheee.page.interceptor.PageInterceptor">
                <!--property指定分页参数-->
                <property name="page.limit" value="2"/>
            </plugin>
        </plugins>
    

    在MyBatis中,只能拦截四种接口的实现类:

    • Executor
    • ParameterHandler
    • ResultSetHandler
    • StatementHandler

    每种类型的拦截方式都是一样的。因此我们需要确定拦截什么对象、什么方法。

    这个需要了解sqlSession的执行原理,可以参考文章:
    MyBatis原理第四篇——statementHandler对象(sqlSession内部核心实现,插件的基础)

    从文中可以知道执行查询是使用StatementHandler的prepare预编译SQL,使用parameterize设置参数,使用query执行查询。

    我们希望的是在预编译前去修改sql,做出加入limit语句限制sql的返回。(这里我用的是Mysql,如果用其他数据库需要自己编写你自己的sql),因此我们要拦截prepare方法。

    我们需要做的,除了在mybatis-config.xml中加入<plugin>,最重要的是编码实现org.apache.ibatis.plugin.Interceptor接口,并指定拦截StatementHandler的prepare()方法。

    @Intercepts(@Signature(
            type = StatementHandler.class,
            method = "prepare",
            args = {Connection.class, Integer.class}
    ))
    public class PageInterceptor implements Interceptor {
    ......
    }
    
    

    type 告诉要拦截什么对象,它可以是四大对象的一个。
    method 告诉你要拦截什么方法。
    args 告诉方法的参数是什么。

    实现拦截器

    在实现前我们需要熟悉一个mybatis中常用的类的使用。它便是:MetaObject。
    它的作用是可以帮助我们取到一些属性和设置属性(包括私有的)。它有三个方法:

    • MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)

    这个方法我们基本不用了,因为MyBATIS中可以用SystemMetaObject.forObject(Object obj)代替它。

    • Object getValue(String name)

    • void setValue(String name, Object value)

    第一个方法是绑定对象,第二个方法是根据路径获取值,第三个方法是获取值。
    这些说还是有点抽象,我们举个例子,比如说现在我有个学生对象(student),它下面有个属性学生证(selfcard),学生证也有个属性发证日期(date)。
    但是发证日期是一个私有的属性且没有提供公共方法访问。我们现在需要访问它,那么我们就可以使用MetaObject将其绑定:

    MetaObject metaStudent = SystemMetaObject.forObject(student);
    这样便可以读取它的属性:
    Date date =(Date) metaStudent.getValue("selfcard.date");
    或者设置它的属性:
    metaStudent.setValue("selfcard.date", new Date());

    MetaObject只是MyBatis一个工具类。实现sql的拦截和修改,其实就是用metaObject.getValue拿到原来的sql,经过修改后再metaObject.setValue设置到Statement中。

    拦截器的实现
    package com.ljheee.page.interceptor;
    
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.reflection.MetaObject;
    
    import java.sql.Connection;
    import java.util.Properties;
    
    import static org.apache.ibatis.reflection.SystemMetaObject.*;
    
    @Intercepts(@Signature(
            type = StatementHandler.class,
            method = "prepare",
            args = {Connection.class, Integer.class}
    ))
    public class PageInterceptor implements Interceptor {
    
        private int limit = 0;
    
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
    
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            MetaObject metaStatementHandler = forObject(statementHandler);
    
            // 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过循环可以分离出最原始的的目标类)
            while (metaStatementHandler.hasGetter("h")) {
                Object object = metaStatementHandler.getValue("h");
                metaStatementHandler = forObject(object);
            }
    
            //BoundSql对象是处理sql语句的。
            String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");
    
            //判断sql是否select语句,如果不是select语句那么就出错了。
            //如果是修改它,是的它最多返回行,这里用的是mysql,其他数据库要改写成其他
            if (sql != null && sql.toLowerCase().trim().indexOf("select") == 0 && !sql.contains("$_$limit_$table_")) {
                //通过sql重写来实现,这里我们起了一个奇怪的别名,避免表名重复.
                sql = "select * from (" + sql + ") $_$limit_$table_ limit " + this.limit;
                metaStatementHandler.setValue("delegate.boundSql.sql", sql); //重写SQL
            }
            return invocation.proceed();//实际就是调用原来的prepared方法,只是再次之前我们修改了sql
        }
    
    
        /**
         *通过Plugin的wrap(...)方法来实现代理类的生成操作
         * @param target
         * @return
         */
        @Override
        public Object plugin(Object target) {
    
            if(target instanceof StatementHandler ){
                return Plugin.wrap(target, this);//使用Plugin的wrap方法生成代理对象
            }else {
                return target;
            }
        }
    
        @Override
        public void setProperties(Properties props) {
            String limitStr = props.get("page.limit").toString();
            this.limit = Integer.parseInt(limitStr);//用传递进来的参数初始化
        }
    
    }
    



    工程 https://github.com/ljheee/mybatis-pages


    PageHelper介绍

    PageHelper分页插件,支持物理分页。
    PageHelper支持多种数据库,如Oracle、MySQL、SqlServer、PostgreSQL等,当前最新版本是4.1.6。

    相关文章

      网友评论

        本文标题:MyBatis:实现简单物理分页(Plugin的使用)

        本文链接:https://www.haomeiwen.com/subject/cxeemftx.html