一步步教你mybatis分页,mybatis分页拦截器 使用,mybatis拦截器分页

          mybatis 分页详解、mybatis分页查询,mybatis分页拦截器使用、struts2下mybatis分页

 


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<AreaCode> list();
        
        //通过RowBounds参数告知mybatis此方法需要分页查询
	public List<AreaCode> list(RowBounds rowBounds);
	
}

3.定义配置文件AreaCodeMapper.xml(配置仅供参考):

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="AreaCodeMapper">
	<select id="list" resultType="AreaCode">
		select provId, description from areacodecfg
	</select>
</mapper>

4.定义配置文件configuration.xml(配置仅供参考):

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="UNPOOLED">
                                <!-- ${driver}为实际的数据库驱动名 -->
				<property name="driver" value="${driver}" />
				<property name="url" value="${url}" />
				<property name="username" value="${username}" />
				<property name="password" value="${password}" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
                <!-- 添加mapper文件 -->
		<mapper resource="AreaCodeMapper.xml" />
	</mappers>
</configuration>

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<AreaCode> 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<String> sortFields, List<String> 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<DBType, IDialect> dialectMap = new HashMap<DBType, IDialect>();
	
	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添加拦截器配置:

<plugins>
		<plugin interceptor="PageStatementInterceptor">
			<property name="DBType" value="mysql" />
		</plugin>
	</plugins>

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<AreaCode> 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<AreaCode> listAreaCode(RowBounds rowBounds) {//方法1
		
		return mapper.list(rowBounds));
		
	}
	
	public List<AreaCode> 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<AreaCode>, 如何将SmartRowBounds的总记录数返回给调用者对象?先不说,后面再谈!

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<AreaCode> list = service.listAreaCode(rb);//使用AreaCodeService的方法1
		setJsonGrid(list, rb.getTotalCount());
		return JSON;
	}
}

只有一个gridList ()方法,通过前台页面传进来的分页参数创建SmartRowBounds交给service进行分页查询,查询完了会通过rb回取总记录数,一起交给父类的setJsonGrid()方法。

上文提到的,将控制器属性gridDataList转化为json对象传到前台jsp,需要在struct.xml中加一段配置,内容片段如下:

<global-results>
    <result name="json" type="json">
	<param name="root">gridDataList</param>
    </result>
</global-results>

spring中mybatis拦截器配置片段:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="xx" />
        <property name="plugins">
        	<array>
                    <!--拦截器类全称-->
        	    <bean class="PageStatementInterceptor"></bean>
        	</array>
        </property>
</bean>

自此所有内容已叙完,最后继续探讨下前面的遗留问题,AreaCodeService的方法2,

如何将查询总数返回到控制器?

其实至少有两个方法:

1.List<AreaCode> listAreaCode(int offset, int limit)再增加第三个参数,然后将SmartRowBounds的记录总数放到第三个参数中,供控制器取,以下是模拟实现:

public List<AreaCode> 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<SmartRowBounds> rowBoundHolder = new ThreadLocal<SmartRowBounds>();
	
	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<AreaCode> 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<AreaCode> list = service.listAreaCode(queryOffset, queryLimit);//使用AreaCodeService的方法2
		setJsonGrid(list);
		return JSON;
	}
}

 All right,如果你觉得有“点”帮助,请点个“赞”哦,3q。

以上实现仅供参考,思路各异,满意就行。

原创博文,转载请注明出处。

猜你喜欢

转载自blog.csdn.net/zhaomin_g/article/details/81190016
今日推荐