mybatis默认是支持分页的,内部通过创建可滚动的ResultSet(ResultSet.TYPE_FORWARD_ONLY)对结果集指针进行跳转以达到分页控制的目的。实际使用,需要传入RowBounds类型参数来告知mybatis做分页控制,RowBounds构造器有两个参数:
RowBounds(int offset, int limit), offset,从第offset条开始查(起始于0),limit查询个数。如:
RowBounds(0, 11):第一页,显示十一条【0-10】、
RowBounds(11, 10):第二页,显示十一条【11-21】
。。。。
不过mybatis默认分页存在两个问题:1.通过ResultSet控制指针进行分页与数据库本身通过sql语句进行分页相比,查询性能欠佳。2.无法返回总记录数,通常情况下前端表格除了要显示第n页数据,也需要显示总记录数,通过内置RowBounds显然不能满足需求。
有些兄弟可能不知道怎么使用,没关系,我先做一个简单的示例,供参考。
问题:现有区号表一张,需要支持表分页查询:
1.定义区号javabean AreaCode.java:
AreaCode.java
public class AreaCode implements Serializable{
//javabean与数据库字段一致
private String provId;
private String description;
public AreaCode() {
super();
}
public String getProvId() {
return provId;
}
public void setProvId(String provId) {
this.provId = provId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
2.定义mapper接口AreaCodeMapper.java(为了简单只定义一个查询接口):
public interface AreaCodeMapper {
//这个方法不支持分页,是全查
public List list();
//通过RowBounds参数告知mybatis此方法需要分页查询
public List list(RowBounds rowBounds);
}
3.定义配置文件AreaCodeMapper.xml(配置仅供参考):
4.定义配置文件configuration.xml(配置仅供参考):
5.测试:
public static void main(String[] args) throws SQLException {
Reader reader = null;
SqlSessionFactory sf;
SqlSession session = null;
try {
SqlSessionFactoryBuilder build = new SqlSessionFactoryBuilder();
//根据配置文件生成SqlSessionFactory
reader = Resources.getResourceAsReader("test/configuration.xml");
sf = build.build(reader);
session = sf.openSession();
AreaCodeMapper mapper = session.getMapper(AreaCodeMapper.class);
//查询第一页,每页十条记录
RowBounds rowBounds = new RowBounds(0, 10);
List list = mapper.list(rowBounds);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
if (session != null) {
session.close();
}
}
}
分页查询已搞定,但以上查询并不包含总记录数,这个比较麻烦,如果你愿意,再添加一个总记录数查询的接口,不愿意接着往下看。
下面通过扩展RowBounds,使其支持数据库分页与总记录数统计。
1.由于不同的数据库,分页sql语句略有不同,我们通过一个枚举类型DBType.java,用于区分不同数据库类型。假设只支持mysql和oracle两种数据库,要添加其他数据库,自己扩展:
public enum DBType {
MYSQL, ORACLE
}
2.创建用于sql转换的接口IDialect.java。接口功能:可将普通查询sql,转换成与具体数据库相关的分页查询sql,IDialect.java:
public interface IDialect {
//支持根据字段进行排序查询,留给读者实现吧
String getSortSQL(String sql, List sortFields, List sortOrders);
//根据原始查询sql生成与数据库相关的查询sql
//原始sql就是上文提到的:select provId, description from areacodecfg
String getLimitSQL(String sql, int offset, int limit);
//根据原始的sql生成查询总记录数的sql
String getTotalSQL(String sql);
}
3.定义IDialect的抽象实现ADialect.java:
public abstract class ADialect implements IDialect {
@Override
public String getTotalSQL(String sql) {//基本所有数据库通用
StringBuffer totalSql = new StringBuffer(sql.length() + 100);
totalSql.append("select count(0) from ( ").append(sql).append(
" ) as _tmp_count");
return totalSql.toString();
}
}
4.定义mysql的实现:
public class MySqlDialect extends ADialect {
@Override
public String getLimitSQL(String sql, int offset, int limit) {
sql = sql.trim();
StringBuffer newSql = new StringBuffer(sql.length() + 100);
newSql.append("select * from (").append(sql).append(
") as _tmp_query limit ").append(offset).append(",").append(limit);
return newSql.toString();
}
}
5.定义oracle的实现:
public class OracleDialect extends ADialect {
@Override
public String getLimitSQL(String sql, int offset, int limit) {
sql = sql.trim();
StringBuffer pageSelect = new StringBuffer(sql.length() + 100);
pageSelect
.append("select * from ( select row_.*, rownum rownum_ from ( ");
pageSelect.append(sql);
pageSelect.append(" ) row_ ) where rownum_ > ").append(offset).append(
" and rownum_ <= ").append(offset + limit);
return pageSelect.toString();
}
}
5.创建IDialect的实例化工厂RoutingDialect.java:
public class RoutingDialect implements IDialect {
//实际委托给delegate
private IDialect delegate;
//用于缓存实例
private static Map dialectMap = new HashMap();
private RoutingDialect(DBType dbType) {
switch (dbType) {
case MYSQL:
delegate = new MySqlDialect();
break;
case ORACLE:
delegate = new OracleDialect();
break;
default:
delegate = new MySqlDialect();
}
}
//工厂方法,根据数据库类型返回相应的Dialect
public static IDialect getDialect(DBType dbType) {
IDialect dialect = null;
dialect = dialectMap.get(dbType);
if (dialect == null) {
synchronized (dialectMap) {
dialect = dialectMap.get(dbType);
if (dialect == null) {
dialect = new RoutingDialect(dbType);
dialectMap.put(dbType, dialect);
}
}
}
return dialect;
}
@Override
public String getLimitSQL(String sql, int offset, int limit) {
return delegate.getLimitSQL(sql, offset, limit);
}
@Override
public String getTotalSQL(String sql) {
return delegate.getTotalSQL(sql);
}
}
6.创建SmartRowBounds扩展自RowBounds,支持数据库分页,与总记录数统计:
public class SmartRowBounds extends RowBounds {
//内部参数:当前查询记录的偏移
private int queryOffset = -1;
//内部参数:当前查询记录的条数
private int queryLimit = -1;
//内部参数:用于保存总记录数
private int totalCount;
//是否使用数据库分页,还是使用mybatis的默认滚动分页
private boolean isDbSupport = false;
//内部标识,是否阻止默认分页,当isDbSupport=true时此标识必须设成true
private boolean preventDefaultRowBounds = false;
public SmartRowBounds() {
//无分页参数,不分页
super(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
}
public SmartRowBounds(int queryOffset, int queryLimit) {
//将分页参数传递给父类,这一点很重要,可用于默认分页
super(queryOffset, queryLimit);
this.queryOffset = queryOffset;
this.queryLimit = queryLimit;
}
public SmartRowBounds(int queryOffset, int queryLimit,
boolean isDbSupport) {
this(queryOffset, queryLimit);
this.isDbSupport = isDbSupport;
}
//根据数据库类型,即原始的查询sql来生成数据库相关的分页sql,
//委托IDialect进行转换
public String getPageSql(DBType dbType, String rawSql) {
IDialect dialet = RoutingDialect.getDialect(dbType);
//只有使用数据库分页的情况下才会对sql进行数据库相关的转换
if (isDbSupport && queryOffset >= 0 && queryLimit >= 0) {
rawSql = dialet.getLimitSQL(rawSql, queryOffset, queryLimit);
//如果使用了数据库分页,就必须阻止mybatis默认分页行为。
//设置一个阻止默认分页的标识preventDefaultRowBounds=tue
preventDefaultRowBounds = true;
}
return rawSql;
}
public String getTotalSQL(DBType dbType, String rawSql) {
return RoutingDialect.getDialect(dbType).getTotalSQL(rawSql);
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public int getTotalCount() {
return totalCount;
}
@Override
public int getLimit() {
//mysql会调用RowBounds的getLimit方法计算查询记录条数,
//如果未使用数据库分页,你需要告诉mybatis查询的记录长度queryLimit
if (!preventDefaultRowBounds) {
return queryLimit;
}
//如果你使用了数据库分页,那么mybatis内部就不能再使用ResultSet滚动分页了
//因此需要返回super.NO_ROW_LIMIT,告知mybatis不进行分页相关的结果集指针跳转
return super.NO_ROW_LIMIT;
}
@Override
public int getOffset() {
//mysql会调用RowBounds的getOffset方法计算查询偏移,
if (!preventDefaultRowBounds) {
return queryOffset;
}
//如果默认使用了数据库分页,那么mybatis内部就不能再使用ResultSet滚动分页了
//因此返回NO_ROW_OFFSET
return super.NO_ROW_OFFSET;
}
public void reset() {
totalCount = 0;
preventDefaultRowBounds = false;
}
}
7.创建mybatis分页拦截器类:
//注意注解,这里只对Connection创建的的preparedStatement进行拦截,固定写法,可以不深究
@Intercepts( { @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class PageStatementInterceptor implements Interceptor {
private DBType dbType;
public void setDBType(String dbTypeStr) {
if (dbTypeStr != null && !"".equals(dbTypeStr.trim())) {
try {
this.dbType = DBType.valueOf(dbTypeStr.trim().toUpperCase());
} catch (Exception e) {
this.dbType = DBType.MYSQL;
}
}
}
//mybatis框架自动调用
@Override
public void setProperties(Properties properties) {
setDBType(properties.getProperty("DBType"));
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
StatementHandler deleStatementHandler = (StatementHandler) getFieldValue(statementHandler, "delegate");
RowBounds rowBounds = (RowBounds) getFieldValue(deleStatementHandler, "rowBounds");
MappedStatement mappedStatement = (MappedStatement)getFieldValue(deleStatementHandler, "mappedStatement");
//只针对select类型切分页参数是SmartRowBounds的查询进行sql改造
if (rowBounds != null && mappedStatement != null &&
SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType()) &&
SmartRowBounds.class.isAssignableFrom(rowBounds.getClass())) {
SmartRowBounds pageHandler = (SmartRowBounds) rowBounds;
//获取原始sql
BoundSql boundSql = statementHandler.getBoundSql();
if (boundSql != null) {
//进行总记录数查询
countTotal(pageHandler, (Connection) invocation.getArgs()[0], statementHandler.getParameterHandler(), boundSql.getSql());
//将原始sql替换为支持分页的sql
setFieldValue(boundSql, "sql", pageHandler.getPageSql(dbType, boundSql.getSql()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return invocation.proceed();
}
private Field getField(Object target, String fieldName) {
Field field = null;
for (Class> clazz = target.getClass(); clazz != Object.class; clazz = clazz
.getSuperclass()) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
// ignore
}
}
if (field != null) {
if (!field.isAccessible()) {
try {
field.setAccessible(true);
} catch (Exception e) {
// ignore
}
}
}
return field;
}
private Object getFieldValue(Object target, String fieldName) {
try {
Field field = getField(target, fieldName);
if (field != null) {
return field.get(target);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void setFieldValue(Object target, String fieldName, Object value) {
try {
Field field = getField(target, fieldName);
if (field != null) {
field.set(target, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void countTotal(SmartRowBounds pageHandler, Connection con, ParameterHandler paramHandler, String rawSql) {
ResultSet rs = null;
PreparedStatement stmt = null;
try {
String totalSql = pageHandler.getTotalSQL(dbType, rawSql);
if (totalSql != null && !totalSql.isEmpty()) {
stmt = con.prepareStatement(totalSql);
paramHandler.setParameters(stmt);
rs = stmt.executeQuery();
if (rs.next()) {
pageHandler.setTotalCount(rs.getInt(1));
}
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e1) {
// ignore
}
try {
if (stmt != null) {
stmt.close();
}
} catch (Exception e) {
// ignore
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
8.configuration.xml添加拦截器配置:
9.测试:
public class Test {
public static void main(String[] args) throws SQLException {
Reader reader = null;
SqlSessionFactory sf;
SqlSession session = null;
try {
SqlSessionFactoryBuilder build = new SqlSessionFactoryBuilder();
reader = Resources.getResourceAsReader("test/configuration.xml");
sf = build.build(reader);
session = sf.openSession();
AreaCodeMapper mapper = session.getMapper(AreaCodeMapper.class);
SmartRowBounds rowBounds = new SmartRowBounds(0, 10, true);
//分页记录与记录总数,使用一次接口调用搞定
List list = mapper.list(rowBounds);
int total = rowBounds.getTotalCount();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
if (session != null) {
session.close();
}
}
}
}
以上介绍了拦截器分页的基本用法,下面简单介绍下,如果配合struts, spring进行分页查询
1.创建业务类AreaCodeService.java,正常情况下应该创建业务接口再创建实现类,为了简单起见,跳过接口创建:
@Service
public class AreaCodeService {
//通过spring进行自动注入
@Resource
private AreaCodeMapper mapper;
public List listAreaCode(RowBounds rowBounds) {//方法1
return mapper.list(rowBounds));
}
public List listAreaCode(int offset, int limit) {//方法2
//直接创建SmartRowBounds
//但是总记录数如何传递出去?
return mapper.list(new SmartRowBounds(offset, limit));
}
}
AreaCodeService很简单,基本就是委托mapper查询,唯一需要做的就是实例化RowBounds交给mapper。
上面代码定义了两个用于分页查询的方法,方法1:通过传入RowBounds类型参数,实现分页控制,好处是简单,实际的rowbounds对象的创建代码放到调用者(一般是structs的控制器对象)中,缺点是AreaCodeService与mybatis耦合太紧,如果以后使用其他orm框架如hibernate,那就需要修改service类。方法2:方法独立性比较好,不与任何特定框架耦合,它只关心查询偏移,与返回记录数,但问题是,方法的返回类型是List
2.创建Struts控制器的抽象Action, AbstractAction.java:
public class AbstractAction extends ActionSupport{
public static final String JSON = "json";
//querOffset与queryLimit是通过前台jsp传过来的分页相关参数
//其实可以将这两个参数封装成Page对象,通过Page对象来取queryOffset与queryLimit
//这里这样操作是为了偷懒,展示用
protected int queryOffset = -1;
protected int queryLimit = -1;
//前台页表格加载需要的数据对象
protected Object gridDataList;
//将分页的查询的结果存放到gridDataList
//最终通过struts.xml配置文件将gridDataList转换为json传到前台jsp
//list为查询的结果, total为总记录数
protected void setJsonGrid(List list, int total) {
Map data = new HashMap();
data.put("total", total);
data.put("rows", list);
this.gridDataList = data;
}
public int getQueryOffset() {
return queryOffset;
}
public void setQueryOffset(int queryOffset) {
this.queryOffset = queryOffset;
}
public int getGridDataList() {
return gridDataList;
}
public void setGridDataList(Object gridDataList) {
this.gridDataList= gridDataList;
}
public int getQueryLimit() {
return queryLimit;
}
public void setQueryLimit(int queryLimit) {
this.queryLimit = queryLimit;
}
}
抽象action继承自Struts的ActionSupport,方法功能看注释,不废话了。
2.创建具体控制器AreaCodeAction.java:
public class AreaCodeAction extends AbstractAction{
//通过spring将AreaCodeService注入进来
@Resource
private AreaCodeService service;
public String gridList() {
SmartRowBounds rb = new SmartRowBounds(queryOffset, queryLimit, true);
List list = service.listAreaCode(rb);//使用AreaCodeService的方法1
setJsonGrid(list, rb.getTotalCount());
return JSON;
}
}
只有一个gridList ()方法,通过前台页面传进来的分页参数创建SmartRowBounds交给service进行分页查询,查询完了会通过rb回取总记录数,一起交给父类的setJsonGrid()方法。
上文提到的,将控制器属性gridDataList转化为json对象传到前台jsp,需要在struct.xml中加一段配置,内容片段如下:
gridDataList
spring中mybatis拦截器配置片段:
自此所有内容已叙完,最后继续探讨下前面的遗留问题,AreaCodeService的方法2,
如何将查询总数返回到控制器?
其实至少有两个方法:
1.List
public List listAreaCode(int offset, int limit, ResultHook hook) {//方法2
SmartRowBounds rb = new SmartRowBounds(offset, limit);
try {
return mapper.list(new SmartRowBounds(offset, limit,true));
} catch (Exception e) {
e.printStackTrace();
} finally {
//将记录总数传给hook,通知到控制器去取
hook.setResult(rb.getTotalCount());
}
}
}
2.借助线程本地上下文ThreadLocal,同一个线程总能共享相同的数据对象,创建RowBoundsHolder对象:
public class RowBoundsHolder {
private static ThreadLocal rowBoundHolder = new ThreadLocal();
public static SmartRowBoundsinstance(int offset, int limit) {
SmartRowBounds rb = new SmartRowBounds(offset, limit);
rowBoundHolder.set(rb);
return rb;
}
public static SmartRowBounds instance(int offset, int limit, boolean supportDbPage) {
SmartRowBounds rb = new SmartRowBounds(offset, limit, supportDbPage);
rowBoundHolder.set(rb);
return rb;
}
public static SmartRowBoundsget() {
return rowBoundHolder.get();
}
}
RowBoundsHolder的instance方法创建一个SmartRowBounds rb并设置到线程本地山下文中,在同一个线程中,调用任意对象的任意方法,总能访问此rb对象的引用。
改造AreaCodeService的方法2:
@Service
public class AreaCodeService {
//通过spring进行自动注入
@Resource
private AreaCodeMapper mapper;
public List listAreaCode(int offset, int limit) {//方法2
return mapper.list(RowBoundsHolder.instance(offset, limit, true));
}
}
改造AbstractAction添加新方法:
protected void setJsonGrid(List list) {
int total = list == null ? 0 : list.size();
SmartRowBounds rb = RowBoundsHolder.get();
if (rb != null) {//通线程上下文获取总记录数
total = rb.getTotalCount();
}
Map data = new HashMap();
data.put("total", total);
data.put("rows", list);
this.gridDataList = data;
}
修改AreaCodeAction.java:
public class AreaCodeAction extends AbstractAction{
//通过spring将AreaCodeService注入进来
@Resource
private AreaCodeService service;
public String gridList() {
List list = service.listAreaCode(queryOffset, queryLimit);//使用AreaCodeService的方法2
setJsonGrid(list);
return JSON;
}
}
All right,如果你觉得有“点”帮助,请点个“赞”哦,3q。
以上实现仅供参考,思路各异,满意就行。
原创博文,转载请注明出处。