【spring boot】開發一個mybatis基於雙數據源的分頁插件

​單數據源的分頁沒什麼說的用現成的pageHelper插件就可以了,而雙數據源因爲存在兩個數據源方言差異的問題,已經無法通過pageHelper搞定,是時候自己搞一個分頁插件了

在上次基於註解實現雙數據源配置的基礎上,我們在加上分頁插件的功能

基本思路:

  • 利用攔截器攔截所有分頁查詢,判斷當前數據源的方言
  • 根據方言拼裝total查詢sql查詢當前查詢的總條數
  • 根據方言拼裝分頁sql,實現分頁查詢

第一步:修改配置文件

在上次雙數據源的配置文件中添加dialect參數,用於動態判斷當前數據源方言

  datasource:
    db1:
      jdbc-url: jdbc:mysql://localhost:3306/mybatis-demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      max-idle: 10
      max-wait: 10000
      min-idle: 5
      initial-size: 5
      dialect: mysql
    db2:
      jdbc-url: jdbc:mysql://localhost:3306/mybatis-demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      max-idle: 10
      max-wait: 10000
      min-idle: 5
      initial-size: 5
      dialect: mysql

第二步:新建一個用於分頁參數類PageParam.java

@Data
public class PageParam {
    public PageParam() {
        pageSize=0;
        pageNum=0;
        total = 0;
    }

    public PageParam(Integer pageNum, Integer pageSize) {
        this.pageNum = pageNum;
        this.pageSize = pageSize;
    }

    private Integer pageNum;
    //    默認每頁顯示條數
    private Integer pageSize;
    //    是否啓用分頁功能
    @JSONField(serialize = false)
    private Boolean useFlag;
    //    是否檢測當前頁碼的合法性(大於最大頁碼或小於最小頁碼都不合法)
    @JSONField(serialize = false)
    private Boolean checkFlag;
    //當前sql查詢的總記錄數,回填
    private Integer total;
    //    當前sql查詢實現分頁後的總頁數,回填
    private Integer totalPage;
    @JSONField(serialize = false)
    private String orderColumn;
    @JSONField(serialize = false)
    private String order;

    public Boolean isUseFlag() {
        return useFlag;
    }
    public Boolean istCheckFlag() {
        return checkFlag;
    }
}

第三步:新建pageConfig.java

用於數據源方言配置信息的加載及根據方言拼裝分頁相關的SQL語句

@Component
public class PageConfig {
    @Value("${spring.datasource.db1.dialect}")
    private String db1Dialect;
    @Value("${spring.datasource.db2.dialect}")
    private String db2Dialect;

    public String getDateSource() {
        return DataSourceContextHolder.getDB();
    }

    public static PageParam getPageParam(Integer pageNum, Integer pageSize) {
        if (pageSize == null || pageNum == null) {
            return null;
        }
        pageNum = pageNum > 0 ? pageNum : 1;
        pageSize = pageSize > 0 ? pageSize : 1;
        return new PageParam(pageNum, pageSize);
    }

    public static PageParam setPageParam(Integer pageNum, Integer pageSize, Map param) {
        PageParam pageParam = getPageParam(pageNum, pageSize);
        if (pageParam != null)
            param.put("pageParam", pageParam);
        return pageParam;
    }

    public String getDialect() {

        String dialect = getDialect(getDateSource());
        return dialect;
    }
    public String getDialect(String dbName){
        if(dbName.equals("db1"))
            return db1Dialect;
        else
            return db2Dialect;
    }

    public String getTotalSqlParam() {
        String sqlParma = " paging";
        if (getDialect().equals("postgre"))
            sqlParma = " as paging";
        else if (getDialect().equals("sqlserver")) {
            sqlParma = " as paging";
        }
        return sqlParma;
    }

    public String getSelectSqlParam(PageParam pageParam) {
        Map param =  getLimitParam(pageParam);
        //默認方言爲mysql
        String sqlParma = " paging_table limit "+(Integer)param.get("first") + " , " +(Integer)param.get("second");
        String dialect = getDialect();
        if (dialect.equals("postgre")) {
            sqlParma = " paging_table limit "+(Integer)param.get("first") + "  offset " +(Integer)param.get("second");
        } else if (dialect.equals("sqlserver")) {
            //sqlserver 分頁需要提供一個字段名 作爲order by的參數
            sqlParma = " as t_" + pageParam.getOrderColumn() + " order by " + pageParam.getOrderColumn() + " " + pageParam.getOrder() + " OFFSET "+(Integer)param.get("first")+" ROWS\n" +
                    "            FETCH NEXT " +(Integer)param.get("second") +" ROWS ONLY";
        }
        else if(dialect.equals("mysql")){
            return sqlParma;
        }
        return sqlParma;
    }
    /**
     * 獲取用於分頁的參數,
     * 一個爲pageSize 分頁大小
     * 一個爲offser(位移) = (pageNum-1)* pageSize
     * 不同數據庫方言語法不同,所以用map做了映射
     * 方便sql拼裝
     */

    public Map getLimitParam(PageParam pageParam) {
        Integer pageSize = pageParam.getPageSize();
        Integer pageNum = pageParam.getPageNum() > 0 ? pageParam.getPageNum() : 1;
        Integer offset = (pageNum - 1) * pageSize;
        Map param = new HashMap();
        String dialect = this.getDialect();
        if (dialect.equals("postgre")) {
            param.put("first", pageSize);
            param.put("second", offset);
        } else if (dialect.equals("mysql") || dialect.equals("sqlserver")) {
            param.put("first", offset);
            param.put("second", pageSize);
        }
        return param;
    }
}

第四步:創建攔截器PageInterceptor

該類主要功能包括2部分:

  1. 查詢總條數
  2. 實現分頁查詢
@Component
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class PageInterceptor implements Interceptor {
    private Logger logger = LoggerFactory.getLogger(getClass().getName());
    @Autowired
    private PageConfig pageConfig;
    //    默認頁碼
    private Integer defaultPageNum = 1;
    //    默認每頁顯示條數
    private Integer defaultPageSize = 20;
    //    是否啓用分頁功能
    private boolean defaultUseFlag = true;
    //    檢測當前頁碼的合法性(大於最大頁碼或小於最小頁碼都不合法)
    private boolean defaultCheckFlag = true;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = getActuralHandlerObject(invocation);
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        String sql = statementHandler.getBoundSql().getSql();

        BoundSql boundSql = statementHandler.getBoundSql();
        Object paramObject = boundSql.getParameterObject();
        if (!checkIsSelectFalg2(paramObject)) {
            return invocation.proceed();
        }
        logger.info("Mybatis 分頁插件當前數據源爲: " + pageConfig.getDateSource());
        logger.info("Mybatis 分頁插件當前數據源方言爲: " + pageConfig.getDialect());
        PageParam pageParam = getPageParam(paramObject);

        if (pageParam == null)
            return invocation.proceed();

        Integer pageNum = pageParam.getPageNum() == null ? defaultPageNum : pageParam.getPageNum();
        Integer pageSize = pageParam.getPageSize() == null ? defaultPageSize : pageParam.getPageSize();

        Boolean useFlag = pageParam.isUseFlag() == null ? defaultUseFlag : pageParam.isUseFlag();
        Boolean checkFlag = pageParam.istCheckFlag() == null ? defaultCheckFlag : pageParam.istCheckFlag();

        //不使用分頁功能
        if (!useFlag) {
            return invocation.proceed();
        }

        int totle = getTotal(invocation, metaStatementHandler, boundSql);

        //將動態獲取到的分頁參數回填到pageParam中
        setTotltToParam(pageParam, totle, pageSize);

        //檢查當前頁碼的有效性
        //checkPage(checkFlag, pageNum, pageParam.getTotalPage());

        //修改sql
        return updateSql2Limit(invocation, metaStatementHandler, boundSql, pageParam);
    }

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

    //    在配置插件的時候配置默認參數
    @Override
    public void setProperties(Properties properties) {
        String strDefaultPage = properties.getProperty("defaultPageNum");
        String strDefaultPageSize = properties.getProperty("defaultPageSize");
        String strDefaultUseFlag = properties.getProperty("defaultUseFlag");
        String strDefaultCheckFlag = properties.getProperty("defaultCheckFlag");
        defaultPageNum = Integer.valueOf(strDefaultPage);
        defaultPageSize = Integer.valueOf(strDefaultPageSize);
        defaultUseFlag = Boolean.valueOf(strDefaultUseFlag);
        defaultCheckFlag = Boolean.valueOf(strDefaultCheckFlag);
    }


    //    從代理對象中分離出真實statementHandler對象,非代理對象
    private StatementHandler getActuralHandlerObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;
//        分離代理對象鏈,目標可能被多個攔截器攔截,分離出最原始的目標類
        while (metaStatementHandler.hasGetter("h")) {
            object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }

        if (object == null) {
            return statementHandler;
        }
        return (StatementHandler) object;
    }

    //    判斷是否是select語句,只有select語句,纔會用到分頁
    private boolean checkIsSelectFalg(String sql) {
        String trimSql = sql.trim();
        int index = trimSql.toLowerCase().indexOf("select");
        return index == 0;
    }

    //有分頁參數,則啓動分頁
    private boolean checkIsSelectFalg2(Object paramerObject) {
        boolean result = false;
        if (paramerObject == null) {
            return false;
        }
        if (paramerObject instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) paramerObject;
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                if (entry.getValue() instanceof PageParam) {
                    result = true;
                    break;
                }
            }
        } else if (paramerObject instanceof PageParam) {
//            繼承方式 pojo繼承自PageParam 只取出我們希望得到的分頁參數
            result = true;
        }
        return result;
    }

    /*
    獲取分頁的參數
    參數可以通過map,@param註解進行參數傳遞。或者請求pojo繼承自PageParam  將PageParam中的分頁數據放進去
     */
    private PageParam getPageParam(Object paramerObject) {
        if (paramerObject == null) {
            return null;
        }

        PageParam pageParam = null;
        //通過map和@param註解將PageParam參數傳遞進來,pojo繼承自PageParam不推薦使用  這裏從參數中提取出傳遞進來的pojo繼承自PageParam
//        首先處理傳遞進來的是map對象和通過註解方式傳值的情況,從中提取出PageParam,循環獲取map中的鍵值對,取出PageParam對象
        if (paramerObject instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) paramerObject;
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                if (entry.getValue() instanceof PageParam) {
                    return (PageParam) entry.getValue();
                }
            }
        } else if (paramerObject instanceof PageParam) {
//            繼承方式 pojo繼承自PageParam 只取出我們希望得到的分頁參數
            pageParam = (PageParam) paramerObject;

        }
        return pageParam;
    }

    //    獲取當前sql查詢的記錄總數
    private int getTotal(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {
//        獲取mapper文件中當前查詢語句的配置信息
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");

        //獲取所有配置Configuration
        org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();

//        獲取當前查詢語句的sql
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");

//        將sql改寫成統計記錄數的sql語句
        String sqlParma = pageConfig.getTotalSqlParam();
        String countSql = "select count(*) as total from (" + sql + ") " + sqlParma;
//        獲取connection連接對象,用於執行countsql語句
        Connection conn = (Connection) invocation.getArgs()[0];
        PreparedStatement ps = null;
        int total = 0;

        try {
//            預編譯統計總記錄數的sql
            ps = conn.prepareStatement(countSql);
            //構建統計總記錄數的BoundSql
            BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            //構建ParameterHandler,用於設置統計sql的參數
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
            //設置總數sql的參數
            parameterHandler.setParameters(ps);
            //執行查詢語句
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
//                與countSql中設置的別名對應
                total = rs.getInt("total");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
        }
        return total;
    }

    //    設置條數參數到pageparam對象
    private void setTotltToParam(PageParam param, int totle, int pageSize) {
        param.setTotal(totle);
        param.setTotalPage(totle % pageSize == 0 ? totle / pageSize : (totle / pageSize) + 1);
    }

    //    修改原始sql語句爲分頁sql語句
    private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, PageParam pageParam) throws InvocationTargetException, IllegalAccessException, SQLException {
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        //構建新的分頁sql語句
        String sqlParma = pageConfig.getSelectSqlParam(pageParam);
        String limitSql = "select * from (" + sql + ") " + sqlParma;
        //修改當前要執行的sql語句
        metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
        //相當於調用prepare方法,預編譯sql並且加入參數
        PreparedStatement ps = (PreparedStatement) invocation.proceed();
        return ps;
    }

    //    驗證當前頁碼的有效性
    private void checkPage(boolean checkFlag, Integer pageNumber, Integer pageTotle) throws Exception {
        if (checkFlag) {
            if (pageNumber > pageTotle) {
                throw new Exception("查詢失敗,查詢頁碼" + pageNumber + "大於總頁數" + pageTotle);
            }
            if (pageNumber < 0) {
                throw new Exception("查詢失敗,查詢頁碼小於0 " + pageNumber );
            }
        }
    }
}

第五步:使用及測試

controller 按年齡查詢employee

    @ApiOperation(value = "分頁查詢")
    @GetMapping("getEmployeeByPage")
    public ResultMsg getEmployeeByPage(Integer age,Integer pageNum,Integer paseSize){
        Map param = new HashMap<>();
        param.put("age",age);
        PageParam pageParam = new PageParam(pageNum,paseSize);
        param.put("pageParam", pageParam);
        List result = employeeMapper.getEmployeeByPage(param);
        return ResultMsg.getMsg(result,pageParam);
    }

注意:分頁插件是通過判斷查詢參數是否有PageParam類型參數,來判斷是否分頁;所以如果要分頁就傳入一個pageParam的參數即可

Mapper 數據源爲db2

    @DS("db2")
    List getEmployeeByPage(Map param);

XML

    <select id="getEmployeeByPage" parameterType="map" resultMap="BaseResultMap">
    select * from employee where age > #{age}
  </select>

打開swagger

在這裏插入圖片描述
返回結果如下:

{
  "data": [
    {
      "address": "北新街5lkgw",
      "age": "26",
      "createTime": 1562061284000,
      "deptId": "4",
      "gender": 1,
      "id": "318397755906347008",
      "name": "hkl2txf41c"
    },
    {
      "address": "北新街n5iaz",
      "age": "27",
      "createTime": 1562061285000,
      "deptId": "5",
      "gender": 1,
      "id": "318397756015398912",
      "name": "edz0ehjnrm"
    }
  ],
  "pageInfo": {
    "pageNum": 2,
    "pageSize": 4,
    "total": 6,
    "totalPage": 2
  },
  "result": "SUCCESS",
  "resultCode": 200,
  "resultMsg": ""
}
Response Code

log打印:

2019-09-20 16:12:23.243  INFO 12092 --- [nio-9393-exec-1] com.wg.demo.common.aop.LogAspect         : Request : {url='http://localhost:9393/mybatis/employee/getEmployeeByPage', ip='0:0:0:0:0:0:0:1', classMethod='com.wg.demo.controller.EmployeeController.getEmployeeByPage', args=[22, 1, 2]}
2019-09-20 16:12:23.243  INFO 12092 --- [nio-9393-exec-1] com.wg.demo.common.aop.LogAspect         : request Param: [22, 1, 2]
2019-09-20 16:12:23.250  INFO 12092 --- [nio-9393-exec-1] c.w.d.c.d.DynamicDataSourceAspect        : 當前數據源爲db2
2019-09-20 16:12:23.263  INFO 12092 --- [nio-9393-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-09-20 16:12:23.478  INFO 12092 --- [nio-9393-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-09-20 16:12:23.483  INFO 12092 --- [nio-9393-exec-1] c.w.d.c.interceptor.PageInterceptor      : Mybatis 分頁插件當前數據源爲: db2
2019-09-20 16:12:23.484  INFO 12092 --- [nio-9393-exec-1] c.w.d.c.interceptor.PageInterceptor      : Mybatis 分頁插件當前數據源方言爲: mysql
2019-09-20 16:12:23.484 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : ==>  Preparing: select count(*) as total from (select * from employee where age > ?) paging 
2019-09-20 16:12:23.525 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : ==> Parameters: 22(Integer)
2019-09-20 16:12:23.564 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : <==      Total: 1
2019-09-20 16:12:23.565 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : ==>  Preparing: select * from (select * from employee where age > ?) paging_table limit 0 , 2 
2019-09-20 16:12:23.566 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : ==> Parameters: 22(Integer)
2019-09-20 16:12:23.572 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : <==      Total: 2

mybatis sql log插件打印sql語句如下:

26  2019-09-20 16:12:23.525 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : ==>
select count(*) as total
 FROM (select *
 FROM employee
 WHERE age > 22) paging;
-----------------------------------------------------------------------------------------------------
27  2019-09-20 16:12:23.566 DEBUG 12092 --- [nio-9393-exec-1] c.w.d.d.E.getEmployeeByPage              : ==>
select *
 FROM (select *
 FROM employee
 WHERE age > 22) paging_table
 LIMIT 0 , 2;
-----------------------------------------------------------------------------------------------------

至此分頁插件開發完畢,目前因爲實際項目中我只用到了mysql、postgre、sqlserver這三種數據庫,所以插件目前只支持這三種方言,需要其他方言支持的朋友可修改PageConfig類的getLimitParam及getSelectSqlParam兩個方法,添加對應方言的sql語句拼裝就可以了

項目地址:

https://github.com/bdqx007/Mybatis_demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章