單數據源的分頁沒什麼說的用現成的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部分:
- 查詢總條數
- 實現分頁查詢
@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語句拼裝就可以了
項目地址: