Mybatis通用分页

分页分为真分页和假分页,而 MyBatis 本身没有提供基于数据库方言的分页功能,而是基于 JDBC 的游标分页,很容易出现性能问题。网上提供的一个解决方案感觉还不错,是基于 MyBatis 本身的插件机制,通过拦截 Sql做分页。

首先,我们需要根据不同数据库来加载不同的分页 SQL ,这里我们参考 Hibernate ,定义一个数据库方言接口

Dialect.java

package com.iflytek.mybatis.page.dialect;

/**
 * @author xdwang
 *
 * @ceate 2012-12-19 下午7:45:24
 *
 * @description 数据库方言接口
 *
 */

public interface Dialect {

    public static enum Type {
        MYSQL {
            public String getValue() {
                return "mysql";
            }
        },
        MSSQL {
            public String getValue() {
                return "sqlserver";
            }
        },
        ORACLE {
            public String getValue() {
                return "oracle";
            }
        }
    }

    /**
     * @descrption 获取分页SQL
     * @author xdwang
     * @create 2012-12-19下午7:48:44
     * @param sql
     *            原始查询SQL
     * @param offset
     *            开始记录索引(从零开始)
     * @param limit
     *            每页记录大小
     * @return 返回数据库相关的分页SQL语句
     */
    public abstract String getPageSql(String sql, int offset, int limit);

}

然后分别定义不同类型数据库的具体分页,这里我们列举3个比较常用的,MySQL、MSSQL、Oracle
MySql5Dialect.java

package com.iflytek.mybatis.page.dialect;

/**
 * @author xdwang
 *
 * @ceate 2012-12-19 下午7:50:44
 *
 * @description MySQL数据库实现
 *
 */
public class MySql5Dialect implements Dialect {

    protected static final String SQL_END_DELIMITER = ";";

    public String getPageSql(String sql, boolean hasOffset) {
        return MySql5PageHepler.getPageSql(sql, -1, -1);
    }

    public String getPageSql(String sql, int offset, int limit) {
        return MySql5PageHepler.getPageSql(sql, offset, limit);
    }

    public boolean supportsLimit() {
        return true;
    }
}

MySql5PageHepler.java

package com.iflytek.mybatis.page.dialect;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author xdwang
 *
 * @ceate 2012-12-19 下午8:41:21
 *
 * @description MySql辅助方法
 *
 */
public class MySql5PageHepler {

    /**
     * @descrption 得到查询总数的sql
     * @author xdwang
     * @create 2012-12-19下午8:41:10
     * @param querySelect
     * @return
     */
    public static String getCountString(String querySelect) {

        querySelect = getLineSql(querySelect);
        int orderIndex = getLastOrderInsertPoint(querySelect);

        int formIndex = getAfterFormInsertPoint(querySelect);
        String select = querySelect.substring(0, formIndex);

        // 如果SELECT 中包含 DISTINCT 只能在外层包含COUNT
        if (select.toLowerCase().indexOf("select distinct") != -1 || querySelect.toLowerCase().indexOf("group by") != -1) {
            return new StringBuffer(querySelect.length()).append("select count(1) count from (").append(querySelect.substring(0, orderIndex)).append(" ) t").toString();
        } else {
            return new StringBuffer(querySelect.length()).append("select count(1) count ").append(querySelect.substring(formIndex, orderIndex)).toString();
        }
    }

    /**
     * 得到最后一个Order By的插入点位置
     *
     * @return 返回最后一个Order By插入点的位置
     */
    private static int getLastOrderInsertPoint(String querySelect) {
        int orderIndex = querySelect.toLowerCase().lastIndexOf("order by");
        if (orderIndex == -1 || !isBracketCanPartnership(querySelect.substring(orderIndex, querySelect.length()))) {
            throw new RuntimeException("My SQL 分页必须要有Order by 语句!");
        }
        return orderIndex;
    }

    /**
     * 得到分页的SQL
     *
     * @param offset
     *            偏移量
     * @param limit
     *            位置
     * @return 分页SQL
     */
    public static String getPageSql(String querySelect, int offset, int limit) {

        querySelect = getLineSql(querySelect);

        String sql = querySelect.replaceAll("[^\\s,]+\\.", "") + " limit " + offset + " ," + limit;

        return sql;

    }

    /**
     * 将SQL语句变成一条语句,并且每个单词的间隔都是1个空格
     *
     * @param sql
     *            SQL语句
     * @return 如果sql是NULL返回空,否则返回转化后的SQL
     */
    private static String getLineSql(String sql) {
        return sql.replaceAll("[\r\n]", " ").replaceAll("\\s{2,}", " ");
    }

    /**
     * 得到SQL第一个正确的FROM的的插入点
     */
    private static int getAfterFormInsertPoint(String querySelect) {
        String regex = "\\s+FROM\\s+";
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(querySelect);
        while (matcher.find()) {
            int fromStartIndex = matcher.start(0);
            String text = querySelect.substring(0, fromStartIndex);
            if (isBracketCanPartnership(text)) {
                return fromStartIndex;
            }
        }
        return 0;
    }

    /**
     * 判断括号"()"是否匹配,并不会判断排列顺序是否正确
     *
     * @param text
     *            要判断的文本
     * @return 如果匹配返回TRUE,否则返回FALSE
     */
    private static boolean isBracketCanPartnership(String text) {
        if (text == null || (getIndexOfCount(text, ‘(‘) != getIndexOfCount(text, ‘)‘))) {
            return false;
        }
        return true;
    }

    /**
     * 得到一个字符在另一个字符串中出现的次数
     *
     * @param text
     *            文本
     * @param ch
     *            字符
     */
    private static int getIndexOfCount(String text, char ch) {
        int count = 0;
        for (int i = 0; i < text.length(); i++) {
            count = (text.charAt(i) == ch) ? count + 1 : count;
        }
        return count;
    }
}

OracleDialect.java

package com.iflytek.mybatis.page.dialect;

/**
 * @author xdwang
 *
 * @ceate 2012-12-19 下午7:54:56
 *
 * @description Oracle数据库实现
 *
 */
public class OracleDialect implements Dialect {

    public String getPageSql(String sql, int offset, int limit) {
        sql = sql.trim();
        boolean isForUpdate = false;
        if (sql.toLowerCase().endsWith(" for update")) {
            sql = sql.substring(0, sql.length() - 11);
            isForUpdate = true;
        }

        StringBuffer pageSql = new StringBuffer(sql.length() + 100);
        pageSql.append("select * from ( select row_.*, rownum rownum_ from ( ");
        pageSql.append(sql);
        pageSql.append(" ) row_ ) where rownum_ > " + offset + " and rownum_ <= " + (offset + limit));
        if (isForUpdate) {
            pageSql.append(" for update");
        }
        return pageSql.toString();
    }

}

SQLServerDialect.java

package com.iflytek.mybatis.page.dialect;

/**
 * @author xdwang
 *
 * @ceate 2012-12-19 下午7:53:14
 *
 * @description SQLServer数据库实现
 *
 */
public class SQLServerDialect implements Dialect {

    public String getPageSql(String sql, int offset, int limit) {
        sql = sql.trim();
        StringBuffer pageSql = new StringBuffer(sql.length() + 100);
        // 其实这里还是有一点问题的,就是排序问题,指定死了,有解决的提供一下,等复习到Hibernate看看Hibernat内部是如何实现的。
        pageSql.append("select * from(select a.*,row_number() over (order by id desc) rownum from( ");
        pageSql.append(sql);
        pageSql.append(") a )b where rownum> " + offset + " and rownum <= " + (offset + limit));
        return pageSql.toString();
    }

}

然后我们定义拦截器
PaginationInterceptor.java

package com.iflytek.mybatis.page.interceptor;

import java.sql.Connection;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
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;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;

import com.iflytek.mybatis.page.dialect.Dialect;
import com.iflytek.mybatis.page.dialect.MySql5Dialect;
import com.iflytek.mybatis.page.dialect.OracleDialect;

/**
 *
 * @author xdwang
 *
 * @ceate 2012-12-19 下午8:01:31
 *
 * @description 然后就是实现mybatis提供的拦截器接口,编写我们自己的分页实现,原理就是拦截底层JDBC操作相关的Statement对象,
 *              把前端的分页参数如当前记录索引和每页大小通过拦截器注入到sql语句中
 *              ,即在sql执行之前通过分页参数重新生成分页sql,而具体的分页sql实现是分离到Dialect接口中去。
 *
 *
 */
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class PaginationInterceptor implements Interceptor {

    private final static Log log = LogFactory.getLog(PaginationInterceptor.class);

    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler);
        RowBounds rowBounds = (RowBounds) metaStatementHandler.getValue("delegate.rowBounds");
        if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        }
        Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
        Dialect.Type databaseType = null;
        try {
            databaseType = Dialect.Type.valueOf(configuration.getVariables().getProperty("dialect").toUpperCase());
        } catch (Exception e) {
            // ignore
        }
        if (databaseType == null) {
            throw new RuntimeException("the value of the dialect property in configuration.xml is not defined : " + configuration.getVariables().getProperty("dialect"));
        }
        Dialect dialect = null;
        switch (databaseType) {
        case MYSQL:
            dialect = new MySql5Dialect();
            break;
        case MSSQL:
            dialect = new MySql5Dialect();
            break;
        case ORACLE:
            dialect = new OracleDialect();
            break;
        default:
            dialect = new MySql5Dialect();
        }

        String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        metaStatementHandler.setValue("delegate.boundSql.sql", dialect.getPageSql(originalSql, rowBounds.getOffset(), rowBounds.getLimit()));
        metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
        metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
        if (log.isDebugEnabled()) {
            log.debug("生成分页SQL : " + boundSql.getSql());
        }
        return invocation.proceed();
    }

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

    public void setProperties(Properties properties) {
    }

}

Ok,搞定了,下面看看如何使用,其实和直接调用MyBatis原生的假分页方式一样。只需要在mybatis-config.xml添加一个标识和一个插件

Xml代码

    <properties>
        <property name="dialect" value="mysql" />
    </properties>

    <plugins>
        <plugin interceptor="com.iflytek.mybatis.page.interceptor.PaginationInterceptor">
        </plugin>
    </plugins>

然后和MyBatis默认提供分页的方式一样,直接调用

Java代码

    public List<Student> getStudentsByPage(){
        List<Student> students = new ArrayList<Student>();
        SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
        try {
            //从第一条开始,取4条记录
            RowBounds rowBounds = new RowBounds(1,4);
            Student student=new Student();
            student.setName("xdwang");
            students = sqlSession.selectList("com.iflytek.dao.mapper.StudentMapper.selectByPageList", student, rowBounds);
            sqlSession.commit();
        } finally {
            sqlSession.close();
        }
        return students;

    }

Ok ,搞定,当然,上面我们也可以将需要拦截添加的 Sql 写在 mapper.xml 中,然后再需要分页的查询语句中引用,只是需要在每个模块下分页的地方都引用,相对来说比较麻烦点(其实也还好)。

时间: 2024-10-20 16:19:13

Mybatis通用分页的相关文章

Springboot集成mybatis通用Mapper与分页插件PageHelper(推荐)

插件介绍 通用 Mapper 是一个可以实现任意 MyBatis 通用方法的框架,项目提供了常规的增删改查操作以及 Example 相关的单表操作.通用 Mapper 是为了解决 MyBatis 使用中 90% 的基本操作,PageHelper则提供通用的分页查询功能,使用它们可以很方便的进行开发,可以节省开发人员大量的时间. 通用Mapper的GIT地址: https://gitee.com/free/Mapper 分页插件的GIT地址: https://github.com/pagehelp

Mybatis 通用Crud-设计思路

一 关于Mybatis 1.1 mybatis 的优点 1 轻量级ORM . 2 提供了完善的缓存机制. 3 mapper.xml 原声SQL更清晰灵活,且sql便于SQL调优. 4 resultType resultMap 处理返回结果集,与pojo解耦. 1.2 mybatis的使用体验 这里只将Mybatis不便于使用之处做以说明. 1 需要为每张表写一个dao接口和mapper.xml,这对于开发者来讲就不是很友好了,假设系统有30张业务表,呵呵. 2 虽然有generator 工具,可

SpringBoot+Mysql+Mybatis+Mybatis通用mapper+PageHelper整合

pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4

ibernate学习笔记5---实体类或属性名与数据库关键字冲突、hql命名参数、hql实现通用分页

一.实体类或属性名与数据库关键字冲突问题1.实体类名与数据库中的关键字冲突比如:实体表User与oracle中的系统表冲突解决方式1:在xml中添加table属性,指定表名,使其不与name默认相等 [html] view plaincopyprint? <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hiber

Mybatis 的分页插件PageHelper-4.1.1的使用

Mybatis 的分页插件 PageHelper 项目地址:http://git.oschina.net/free/Mybatis_PageHelper 文档地址:http://git.oschina.net/free/Mybatis_PageHelper/blob/master/wikis/HowToUse.markdown 我用的版本是PageHelper-4.1.1.Mybatis-3.3.0 PageHelper 依赖于 jsqlparser-0.9.4.jar 使用方法: 1.根据My

通用分页存储过程

/****** Object: StoredProcedure [dbo].[sp_CommonPaging] Script Date: 08/03/2015 21:06:14 ******/ --通用分页存储过程 SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO Create PROCEDURE [dbo].[sp_CommonPaging] ( @tn nvarchar(30),--表名称 @idn nvarchar(20),--表主键名称 @

SQL SERVER 通用分页存储过程

SQL SERVER 通用分页存储过程 从SQLSERVER 2005开始,提供了Row_Number()函数,利用函数生成的Index来处理分页,按照正常的逻辑思维都是传pageIndex和pageSize来完成分页,昨天前端和我沟通,他们使用jQuery.DataTable.js插件,而且经过了公司底层的封装,pageIndex需要变动一下,变成pageIndex*pageSize来传. 也就是说按每页显示30条算,第一次传0,第二次传30这样来计算,我也是醉了. 1.传pageIndex和

mybatis常用分页插件,快速分页处理

在未分享整个查询分页的执行代码之前,先了解一下执行流程. 1.总体上是利用mybatis的插件拦截器,在sql执行之前拦截,为查询语句加上limit X X 2.用一个Page对象,贯穿整个执行流程,这个Page对象需要用java编写前端分页组件 3.用一套比较完整的三层entity,dao,service支持这个分页架构 4.这个分页用到的一些辅助类 注:分享的内容较多,这边的话我就不把需要的jar一一列举,大家使用这个分页功能的时候缺少什么就去晚上找什么jar包即可,尽可能用maven包导入

MyBatis通用Mapper开发

MyBatis通用Mapper开发 通常情况下,MyBatis 的增删改查操作需要自己在相应xml中写相关语句,但是运用相关工具,其实可以很方便的自动生成单表的所有增删改查(通用的多表联合查询还是需要自己写).也可以根据具体环境,设计相关模板,自动生成符合要求的controller和service. 使用教程:http://git.oschina.net/free/Mapper#mybatis通用mapper3具体实践:后台权限通用框架