服务器之家

服务器之家 > 正文

Mybatis全面分页插件

时间:2020-06-06 14:57     来源/作者:junehappylove

根据下面分页的思想,很容易实现Mybitas的多租户设计。 

使用Mybatis提供的拦截器。对分页的SQL语句通过封装处理,处理成不同的分页sql。 

本例已经实现了对Mysql和Oracle的分页功能。注意下面的引用包,不要引用错了。 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
 
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
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.scripting.defaults.DefaultParameterHandler;
 
import com.yidao.utils.Page;
import com.yidao.utils.ReflectHelper;
 
/**
 *
 * 分页拦截器,用于拦截需要进行分页查询的操作,然后对其进行分页处理。
 * 利用拦截器实现Mybatis分页的原理:
 * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象,而且对应的Sql语句
 * 是在Statement之前产生的,所以我们就可以在它生成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的
 * prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用
 * StatementHandler对象的prepare方法,即调用invocation.proceed()。
 * 对于分页而言,在拦截器里面我们还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设
 * 置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计。
 *
 */
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
public class PageInterceptor implements Interceptor {
 private String dialect = ""; //数据库方言
 private String pageSqlId = ""; //mapper.xml中需要拦截的ID(正则匹配)
  
 public Object intercept(Invocation invocation) throws Throwable {
  //对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另一个是抽象类BaseStatementHandler,
  //BaseStatementHandler有三个子类,分别是SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler,
  //SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是
  //处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个
  //StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、
  //PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。
  //我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候
  //是通过Interceptor的plugin方法进行包裹的,所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。
  if(invocation.getTarget() instanceof RoutingStatementHandler){
   RoutingStatementHandler statementHandler = (RoutingStatementHandler)invocation.getTarget();
   StatementHandler delegate = (StatementHandler) ReflectHelper.getFieldValue(statementHandler, "delegate");
   BoundSql boundSql = delegate.getBoundSql();
   Object obj = boundSql.getParameterObject();
   if (obj instanceof Page<?>) {
    Page<?> page = (Page<?>) obj;
    //通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
    MappedStatement mappedStatement = (MappedStatement)ReflectHelper.getFieldValue(delegate, "mappedStatement");
    //拦截到的prepare方法参数是一个Connection对象
    Connection connection = (Connection)invocation.getArgs()[0];
    //获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句
    String sql = boundSql.getSql();
    //给当前的page参数对象设置总记录数
    this.setTotalRecord(page,
      mappedStatement, connection);
    //获取分页Sql语句
    String pageSql = this.getPageSql(page, sql);
    //利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句
    ReflectHelper.setFieldValue(boundSql, "sql", pageSql);
   }
  }
  return invocation.proceed();
 }
 
 /**
  * 给当前的参数对象page设置总记录数
  *
  * @param page Mapper映射语句对应的参数对象
  * @param mappedStatement Mapper映射语句
  * @param connection 当前的数据库连接
  */
 private void setTotalRecord(Page<?> page,
   MappedStatement mappedStatement, Connection connection) {
  //获取对应的BoundSql,这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。
  //delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。
  BoundSql boundSql = mappedStatement.getBoundSql(page);
  //获取到我们自己写在Mapper映射语句中对应的Sql语句
  String sql = boundSql.getSql();
  //通过查询Sql语句获取到对应的计算总记录数的sql语句
  String countSql = this.getCountSql(sql);
  //通过BoundSql获取对应的参数映射
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  //利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。
  BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
  //通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象
  ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);
  //通过connection建立一个countSql对应的PreparedStatement对象。
  PreparedStatement pstmt = null;
  ResultSet rs = null;
  try {
   pstmt = connection.prepareStatement(countSql);
   //通过parameterHandler给PreparedStatement对象设置参数
   parameterHandler.setParameters(pstmt);
   //之后就是执行获取总记录数的Sql语句和获取结果了。
   rs = pstmt.executeQuery();
   if (rs.next()) {
    int totalRecord = rs.getInt(1);
    //给当前的参数page对象设置总记录数
    page.setTotalRecord(totalRecord);
   }
  } catch (SQLException e) {
   e.printStackTrace();
  } finally {
   try {
    if (rs != null)
     rs.close();
    if (pstmt != null)
     pstmt.close();
   } catch (SQLException e) {
    e.printStackTrace();
   }
  }
 }
 
 /**
  * 根据原Sql语句获取对应的查询总记录数的Sql语句
  * @param sql
  * @return
  */
 private String getCountSql(String sql) {
  int index = sql.indexOf("from");
  return "select count(*) " + sql.substring(index);
 }
 
 /**
  * 根据page对象获取对应的分页查询Sql语句,这里只做了两种数据库类型,Mysql和Oracle
  * 其它的数据库都 没有进行分页
  *
  * @param page 分页对象
  * @param sql 原sql语句
  * @return
  */
 private String getPageSql(Page<?> page, String sql) {
  StringBuffer sqlBuffer = new StringBuffer(sql);
  if ("mysql".equalsIgnoreCase(dialect)) {
   return getMysqlPageSql(page, sqlBuffer);
  } else if ("oracle".equalsIgnoreCase(dialect)) {
   return getOraclePageSql(page, sqlBuffer);
  }
  return sqlBuffer.toString();
 }
 
 /**
 * 获取Mysql数据库的分页查询语句
 * @param page 分页对象
 * @param sqlBuffer 包含原sql语句的StringBuffer对象
 * @return Mysql数据库分页语句
 */
 private String getMysqlPageSql(Page<?> page, StringBuffer sqlBuffer) {
  //计算第一条记录的位置,Mysql中记录的位置是从0开始的。
// System.out.println("page:"+page.getPage()+"-------"+page.getRows());
  int offset = (page.getPage() - 1) * page.getRows();
  sqlBuffer.append(" limit ").append(offset).append(",").append(page.getRows());
  return sqlBuffer.toString();
 }
 
 /**
 * 获取Oracle数据库的分页查询语句
 * @param page 分页对象
 * @param sqlBuffer 包含原sql语句的StringBuffer对象
 * @return Oracle数据库的分页查询语句
 */
 private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) {
  //计算第一条记录的位置,Oracle分页是通过rownum进行的,而rownum是从1开始的
  int offset = (page.getPage() - 1) * page.getRows() + 1;
  sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getRows());
  sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset);
  //上面的Sql语句拼接之后大概是这个样子:
  //select * from (select u.*, rownum r from (select * from t_user) u where rownum < 31) where r >= 16
  return sqlBuffer.toString();
 }
 
  
 /**
  * 拦截器对应的封装原始对象的方法
  */
 public Object plugin(Object arg0) {
  // TODO Auto-generated method stub
  if (arg0 instanceof StatementHandler) {
   return Plugin.wrap(arg0, this);
  } else {
   return arg0;
  }
 }
 
 /**
  * 设置注册拦截器时设定的属性
  */
 public void setProperties(Properties p) {
  
 }
 
 public String getDialect() {
 return dialect;
 }
 
 public void setDialect(String dialect) {
 this.dialect = dialect;
 }
 
 public String getPageSqlId() {
 return pageSqlId;
 }
 
 public void setPageSqlId(String pageSqlId) {
 this.pageSqlId = pageSqlId;
 }
 
}

xml配置: 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- MyBatis 接口编程配置 -->
 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <!-- basePackage指定要扫描的包,在此包之下的映射器都会被搜索到,可指定多个包,包与包之间用逗号或分号分隔-->
  <property name="basePackage" value="com.yidao.mybatis.dao" />
  <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
 </bean>
 
 <!-- MyBatis 分页拦截器-->
 <bean id="paginationInterceptor" class="com.mybatis.interceptor.PageInterceptor">
  <property name="dialect" value="mysql"/>
  <!-- 拦截Mapper.xml文件中,id包含query字符的语句 -->
  <property name="pageSqlId" value=".*query$"/>
 </bean>

Page类

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.yidao.utils;
 
 
/**自己看看,需要什么字段加什么字段吧*/
public class Page {
 
 private Integer rows;
 
 private Integer page = 1;
 
 private Integer totalRecord;
 
 public Integer getRows() {
 return rows;
 }
 
 public void setRows(Integer rows) {
 this.rows = rows;
 }
 
 public Integer getPage() {
 return page;
 }
 
 public void setPage(Integer page) {
 this.page = page;
 }
 
 public Integer getTotalRecord() {
 return totalRecord;
 }
 
 public void setTotalRecord(Integer totalRecord) {
 this.totalRecord = totalRecord;
 }
 
}

ReflectHelper类

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.yidao.utils;
 
import java.lang.reflect.Field;
 
import org.apache.commons.lang3.reflect.FieldUtils;
 
public class ReflectHelper {
 
 public static Object getFieldValue(Object obj , String fieldName ){
 
 if(obj == null){
 return null ;
 }
 
 Field targetField = getTargetField(obj.getClass(), fieldName);
 
 try {
 return FieldUtils.readField(targetField, obj, true ) ;
 } catch (IllegalAccessException e) {
 e.printStackTrace();
 }
 return null ;
 }
 
 public static Field getTargetField(Class<?> targetClass, String fieldName) {
 Field field = null;
 
 try {
 if (targetClass == null) {
 return field;
 }
 
 if (Object.class.equals(targetClass)) {
 return field;
 }
 
 field = FieldUtils.getDeclaredField(targetClass, fieldName, true);
 if (field == null) {
 field = getTargetField(targetClass.getSuperclass(), fieldName);
 }
 } catch (Exception e) {
 }
 
 return field;
 }
 
 public static void setFieldValue(Object obj , String fieldName , Object value ){
 if(null == obj){return;}
 Field targetField = getTargetField(obj.getClass(), fieldName);
 try {
 FieldUtils.writeField(targetField, obj, value) ;
 } catch (IllegalAccessException e) {
 e.printStackTrace();
 }
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

标签:

相关文章

热门资讯

歪歪漫画vip账号共享2020_yy漫画免费账号密码共享
歪歪漫画vip账号共享2020_yy漫画免费账号密码共享 2020-04-07
沙雕群名称大全2019精选 今年最火的微信群名沙雕有创意
沙雕群名称大全2019精选 今年最火的微信群名沙雕有创意 2019-07-07
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
玄元剑仙肉身有什么用 玄元剑仙肉身境界等级划分
玄元剑仙肉身有什么用 玄元剑仙肉身境界等级划分 2019-06-21
男生常说24816是什么意思?女生说13579是什么意思?
男生常说24816是什么意思?女生说13579是什么意思? 2019-09-17
返回顶部