ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

MyBatis-编写自定义分页插件

2021-06-04 18:01:23  阅读:236  来源: 互联网

标签:插件 return 分页 自定义 class boundSql sql MyBatis page


 一、基础知识

本文测试和源码分析参考版本: Mybatis-version:3.5.5 本文相关测试源代码:https://github.com/wuya11/mybatis_demo

1.1 参考方向

自定义实现分页插件,参考方向如下:
  1. 编写一个分页(Page)基础对象;
  2. 基于插件原理,自定义一个分页拦截插件;
  3. 基于拦截器,获取BoundSql对象 ,获取动态生成的SQL语句以及相应的参数信息;
  4. 根据参数信息,判断是否需要分页查询;
  5. 生成统计总数的sql,并查询出总条数;
  6. 更新BoundSql对象的数据,设置查询明细sql,加上分页标识;
  7. 写好的分页插件配置到MyBatis中;

1.2 思考维度

  1. 生成统计总数语句时,如何保证select count(1)的性能更好;参考方向:详解分页组件中查count总记录优化
  2. 当查询出统计总数为零时,有何优雅的办法,不再去查询一次明细信息;

二、编码实现

2.1 创建Page对象

  1. 设置常用分页的基础属性字段;
  2. 当不想使用框架默认的自动分页,设置一个可变参数autoCount,可单独查询总数,查询明细组合处理。
/**
 * 分页类
 *
 * @author wl
 */
@Data
public class Page implements Serializable {
    /**
     * 每页显示数量
     */
    @JsonProperty("per_page")
    private int pageSize;
    /**
     * 当前页码
     */
    @JsonProperty("current_page")
    private int curPage;
    /**
     * 总页数
     */
    @JsonProperty("total_pages")
    private int pages;
    /**
     * 总记录数
     */
    private int total;
    /**
     * 当前页数量
     */
    private int count;
    /**
     * 链接
     */
    private Link links;

    /**
     * 自动统计分页总数
     */
    private boolean autoCount;

    /**
     * 默认无参构造器,初始化各值
     */
    public Page() {
        this.pageSize = 20;
        this.curPage = 1;
        this.pages = 0;
        this.total = 0;
        this.count = 0;
        this.autoCount = true;
    }

    public Page(Page page) {
        this.pageSize = page.pageSize;
        this.curPage = page.curPage;
        this.pages = page.pages;
        this.total = page.total;
        this.count = page.count;
        this.links = page.links;
        this.autoCount = page.autoCount;
    }

    public void calculate(int total) {
        this.setTotal(total);
        this.pages = (total / pageSize) + ((total % pageSize) > 0 ? 1 : 0);
        // 如果当前页码超出总页数,自动更改为最后一页
        //this.curPage = this.curPage > pages ? this.pages : this.curPage;
        if (curPage > pages) {
            throw new IllegalStateException("超出查询范围");
        }
    }

    /**
     * 获取分页起始位置和偏移量
     *
     * @return 分页起始位置和偏移量数组
     */
    public int[] paginate() {
        // 数量为零时,直接从0开始
        return new int[]{total > 0 ? (curPage - 1) * pageSize : 0, pageSize};
    }
 }
View Code

2.2 创建分页插件

先构建一个普通插件,在准备prepare sql时,设置拦截。也可以在query sql时设置拦截。
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
  // 或者:
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})
拦截的方法不同,在拦截时获取的参数不同。逻辑会存在细微的区别。
/**
 * 分页SQL插件
 *
 * @author wl
 * @date 2021-5-26
 */
@Intercepts(
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
)
public class PagePlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 分页插件拦截处理
        useMetaObject(invocation);
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
View Code

2.3 分页拦截函数

参考MyBatis插件原理,基于Plugin实现,要获取sql相关的信息,可通过MyBatis自带的MetaObject去获取属性和设置属性。(MetaObject内部基于反射获取属性值,设置属性值)。基于MetaObject获取StatementHandler对象信息,参考如图: MetaObject获取MyBatis运行对象信息参考文档:Mybatis3详解(十四)----Mybatis的分页Mybatis分页拦截原理 编写一个函数,实现更新查询明细sql的功能,当返回数量<1时,不查询明细sql,指定查询一个特殊sql的功能。代码如下:
private void useMetaObject(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    // 调用MetaObject 反射类处理
    //分离代理对象链
    while (metaObject.hasGetter("h")) {
        Object obj = metaObject.getValue("h");
        metaObject = SystemMetaObject.forObject(obj);
    }
    while (metaObject.hasGetter("target")) {
        Object obj = metaObject.getValue("target");
        metaObject = SystemMetaObject.forObject(obj);
    }
    BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
    // 存在分页标识
    Page page = getPage(boundSql);
    if (Objects.nonNull(page)) {
        int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
        if (total <= 0) {
            // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
            metaObject.setValue("delegate.boundSql.sql", "select * from (select 0 as id) as temp where  id>0");
            metaObject.setValue("delegate.boundSql.parameterMappings", Collections.emptyList());
            metaObject.setValue("delegate.boundSql.parameterObject", null);
        } else {
            page.calculate(total);
            String sql = boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", sql);
        }
    }
}

2.4 辅助函数

判断是否存在page

/***
 * 获取分页的对象
 * @param boundSql 执行sql对象
 * @return 分页对象
 */
private Page getPage(BoundSql boundSql) {
    Object obj = boundSql.getParameterObject();
    if (Objects.isNull(obj)) {
        return null;
    }
    Page page = null;
    if (obj instanceof Page) {
        page = (Page) obj;
    } else if (obj instanceof Map) {
        // 如果Dao中有多个参数,则分页的注解参数名必须是page
        try {
            page = (Page) ((Map) obj).get("page");
        } catch (Exception e) {
            return null;
        }
    }
    // 不存在分页对象,则忽略下面的分页逻辑
    if (Objects.nonNull(page) && page.isAutoCount()) {
        return page;
    }
    return null;
}

获取统计总数的sql

/***
 * 获取统计sql
 * @param originalSql 原始sql
 * @return 返回统计加工的sql
 */
private String getCountSql(String originalSql) {
     // 统一转换为小写
    originalSql = originalSql.trim().toLowerCase();
    // 判断是否存在 limit 标识
    boolean limitExist = originalSql.contains("limit");
    if (limitExist) {
        originalSql = originalSql.substring(0, originalSql.indexOf("limit"));
    }
    boolean distinctExist = originalSql.contains("distinct");
    boolean groupExist = originalSql.contains("group by");
    if (distinctExist || groupExist) {
        return "select count(1) from (" + originalSql + ") temp_count";
    }
    // 去掉 order by
    boolean orderExist = originalSql.contains("order by");
    if (orderExist) {
        originalSql = originalSql.substring(0, originalSql.indexOf("order by"));
    }
    // todo   left join还可以考虑优化
    int indexFrom = originalSql.indexOf("from");
    return "select count(*)  " + originalSql.substring(indexFrom);
}

查询总数

/**
 * 查询总记录数
 *
 * @param statementHandler mybatis sql 对象
 * @param conn             链接信息
 */
private int getTotalSize(StatementHandler statementHandler, Connection conn) {
    ParameterHandler parameterHandler = statementHandler.getParameterHandler();
    String countSql = getCountSql(statementHandler.getBoundSql().getSql());
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
        pstmt = (PreparedStatement) conn.prepareStatement(countSql);
        parameterHandler.setParameters(pstmt);
        rs = pstmt.executeQuery();
        if (rs.next()) {
            // 设置总记录数
            return rs.getInt(1);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        try {
            if (rs != null) {
                rs.close();
            }
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return 0;
}

2.5 运行效果

参考上一篇文档中的方式,配置分页插件到MyBatis中。调用测试代码,效果如下: 设置Mapper查询方法
/**
 * 获取进项税信息
 *
 * @param kid 单号
 * @param page 分页参数
 * @return 结果
 */
@SelectProvider(type = LifeLogSqlProvider.class, method = "listInputTaxSql")
List<TaxInput> listInputTax(@Param("kid") Integer kid, @Param("page") Page page);
设置具体查询sql
public String listInputTaxSql(@Param("kid") Integer kid, @Param("page") Page page){
    return new SQL()
            .select("input_tax_id, k_id,sup_id,k_sup_id,org_id,a.tax,invoice_title,remark")
            .from("tx_sup_goods_input_tax a")
            .innerJoin("tx_tax b on a.tax=b.tax")
            .where(kid>0,"a.k_id = #{kid}")
            .orderBy("a.k_id desc")
            .build();
}

 设置查询接口

/**
 * 获取测试税务信息
 *
 * @return 返回存储数据
 */
@GetMapping("/tax")
public List<TaxInput> listInputTax(int kid, Page page) {
    page.setAutoCount(true);
    List<TaxInput> taxInputList  = lifeLogMapper.listInputTax(kid, page);
    if(page.getTotal()==0){
        return Collections.emptyList();
    }else{
        return taxInputList;
    }
}
启动项目,运行效果如图

2.6 扩展

按照上述方案,自定义分页插件测试通过,功能开发完成。 要实现分页,需更新sql,在返回数量为零时,还指定了特殊的sql。一切功能都是基于MetaObject反射类,获取对象,设置对象。但仔细观察需要的BoundSql,却发现与其他对象有一点不同。如图:为什么前面是一个绿色的小旗子? 再次去分析StatementHandler这个接口,发现事情并没有这样复杂,要获取BoundSql,源码本来就提供了get方法。所以,根本不用通过MetaObject获取BoundSql对象。 查看BoundSql具体对象,发现设置sql,设置参数的变量没有提供set方法,不允许调用修改。既然这样,可利用反射重新设置BoundSql属性。 分页拦截函数版本2
private void useReflection(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = statementHandler.getBoundSql();
    // 存在分页标识
    Page page = getPage(boundSql);
    if (Objects.nonNull(page)) {
        int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
        if (total <= 0) {
            // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
            Field fieldParameterMappings = BoundSql.class.getDeclaredField("parameterMappings");
            fieldParameterMappings.setAccessible(true);
            fieldParameterMappings.set(boundSql, Collections.emptyList());

            Field fieldSql = BoundSql.class.getDeclaredField("sql");
            fieldSql.setAccessible(true);
            fieldSql.set(boundSql, "select * from (select 0 as id) as temp where  id>0");

            Field fieldParameterObject = BoundSql.class.getDeclaredField("parameterObject");
            fieldParameterObject.setAccessible(true);
            fieldParameterObject.set(boundSql, null);
        } else {
            page.calculate(total);
            Field field = BoundSql.class.getDeclaredField("sql");
            field.setAccessible(true);
            // 设置分页的SQL代码
            field.set(boundSql, boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize());
        }
    }
}
分页拦截函数版本3
private void useMetaObjectPlus(Invocation invocation) throws Throwable {
    StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = statementHandler.getBoundSql();
    // 存在分页标识
    Page page = getPage(boundSql);
    if (Objects.nonNull(page)) {
        int total = getTotalSize(statementHandler, (Connection) invocation.getArgs()[0]);
        MetaObject metaObject = SystemMetaObject.forObject(boundSql);
        if (total <= 0) {
            // 返回数量小于零,查询一个简单的sql,不去执行明细查询 【基于反射,重新设置boundSql】
            metaObject.setValue("sql", "select * from (select 0 as id) as temp where  id>0");
            metaObject.setValue("parameterMappings", Collections.emptyList());
            metaObject.setValue("parameterObject", null);
        } else {
            page.calculate(total);
            boolean limitExist = boundSql.getSql().trim().toLowerCase().contains("limit");
            if (!limitExist) {
                String sql = boundSql.getSql() + " limit " + (page.getCurPage() - 1) * page.getPageSize() + ", " + page.getPageSize();
                metaObject.setValue("sql", sql);
            }
        }
    }
}
综合对比,推荐分页拦截函数版本3

三、功能扩展

3.1 基于注解配置分页

自定义一个注解,根据方法上是否有注解,来做自动分页处理。核心功能是如何判断方法上是否存在注解,参考源码,拦截获取MappedStatement,获取具体执行的方法,基于反射获取注解信息,参考代码如下:
/***
 * 查看注解的自定义插件是否存在
 * @param mappedStatement 参数
 * @return 返回检查结果
 * @throws Throwable 抛出异常
 */
private boolean existEnhancer(MappedStatement mappedStatement) throws Throwable {
    //获取执行方法的位置
    String namespace = mappedStatement.getId();
    //获取mapper名称
    String className = namespace.substring(0, namespace.lastIndexOf("."));
    //获取方法名aClass
    String methodName = namespace.substring(namespace.lastIndexOf(".") + 1);
    Class<?> aClass = Class.forName(className);
    for (Method method : aClass.getDeclaredMethods()) {
        if (methodName.equals(method.getName())) {
            // 暂不考虑方法被重载
            Enhancer enhancer = method.getAnnotation(Enhancer.class);
            if (Objects.nonNull(enhancer) && enhancer.autoPageCount()) {
                // 设置page
                return true;
            }
        }
    }
    return false;
}

3.2 基于查询参数-判断参数是否包含page对象

  1. 若查询条件中,本身就包括page对象,如何获取page对象?
  2. 若查询对象本身继承自Page,如何获取信息page对象?
要满足分页,必须要存在分页基本的查询参数(每一页数量,当前查询页码),拦截系统中本身的参数对象,主要是通过BoundSql类,获取参数信息。参考代码如下:
/***
 * 获取分页的对象
 * @param boundSql 执行sql对象
 * @return 分页对象
 */
private Page getPage(BoundSql boundSql) {
    Page page = null;
    // 参考源码,调试发现为一个map对象
    Map<String, Object> parameterList = (Map<String, Object>) boundSql.getParameterObject();
    if (Objects.isNull(parameterList)) {
        return null;
    }
    for (Map.Entry<String, Object> entry : parameterList.entrySet()) {
        if (entry.getValue() instanceof Page) {
            page = (Page) entry.getValue();
            break;
        }
    }
    if (Objects.nonNull(page)) {
        return page;
    }
    return null;
}

3.3 插件代码说明

  1. PageAnnotationExecutorPlugin:表示结合注解,基于Executor.class的query方法做拦截,实现分页功能。
  2. PageAnnotationPlugin:表示结合注解,基于StatementHandler.class的prepare方法做拦截,实现分页。该方案主要是调用MetaObject,反射获取对象和设置对象,在不同的代理时,获取到对应对象的模式存在差异(h,target嵌套层不同),存在基于本例获取不到对象的情况。
  3. PagePlugin:基于StatementHandler.class的prepare方法做拦截。

四、思考总结

  1. 应当去了解一下比较优秀的MyBatis分页插件,查看源码,学习参考。
  2. 若项目允许,还是集成成熟的分页插件,自定义的分页插件难免存在一些不足。
  3. 获取类属性时,可基于对象,通过反射获取到对应的类,若对象是基于代理(jdk,cglb)生成的,又该如何获取?
  4. 反射可以获取具体执行方法上的注解,获取方法名称,获取参数类型,等具体参考反射的提供的api接口。
  5. 当统计总数<1时,是否可以让MyBatis返回一个空集合?暂未找到办法,默认一个简单sql的模式,是一种非主流的方式。
  6. 自定义插件的两个关键知识点:MappedStatement,BoundSql。
  7. 基于Executor.class和StatementHandler.class在不同的点做拦截时,拦截到的参数不同,获取MappedStatement,BoundSql的方式不同,需查看源码具体分析。
  8. 为什么在StatementHandler.class的prepare方法做拦截时,反射重新设置BoundSql对象,就可以更新后续执行的sql信息了,但在Executor.class的query方法做拦截时,反射重新设置BoundSql对象不行,需要重新更新MappedStatement对象?
  9. 编写函数时,尽量抽象出通用的辅助函数,每一个辅助函数只做单一的功能。上述三种分页拦截函数实现调整了,都可以使用辅助函数。改动量小。
  10. 关于编码规范,强烈推荐书籍:《重构-改善既有代码的设计结构》,《代码整洁之道》。

标签:插件,return,分页,自定义,class,boundSql,sql,MyBatis,page
来源: https://www.cnblogs.com/wlandwl/p/mybatis_page.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有