分享一个完整的Mybatis分页解决方案
Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。
我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,阅读源码帮助更大minglisoft.cn/technology
【背景】
项目框架是 SpringMVC+Mybatis, 需求是想采用自定义的分页标签,同时,要尽量少的影响业务程序开发的。
如果你已经使用了JS框架( 如:Ext,EasyUi等)自带的分页机能,是属于前端分页,不在本文讨论范围。
【关于问题】
大多数分页器会使用在查询页面,要考虑以下问题:
1)分页时是要随时带有最近一次查询条件
2)不能影响现有的sql,类似aop的效果
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
4)尽量少的影响现有service等接口
【关于依赖库】
Google Guava 作为基础工具包
Commons JXPath 用于对象查询 (1/23日版改善后,不再需要)
Jackson 向前台传送Json格式数据转换用
【关于适用数据库】
现在只适用mysql
(如果需要用在其他数据库可参考 paginator的Dialect部分,改动都不大)
首先是Page类,比较简单,保存分页相关的所有信息,涉及到分页算法。虽然“其貌不扬”,但很重要。后面会看到这个page类对象会以“信使”的身份出现在全部与分页相关的地方。
- /**
 - * 封装分页数据
 - */
 - import java.util.List;
 - import java.util.Map;
 - import org.codehaus.jackson.map.ObjectMapper;
 - import org.slf4j.Logger;
 - import org.slf4j.LoggerFactory;
 - import com.google.common.base.Joiner;
 - import com.google.common.collect.Lists;
 - import com.google.common.collect.Maps;
 - public class Page {
 - private static final Logger logger = LoggerFactory.getLogger(Page.class);
 - private static ObjectMapper mapper = new ObjectMapper();
 - public static String DEFAULT_PAGESIZE = "10";
 - private int pageNo; //当前页码
 - private int pageSize; //每页行数
 - private int totalRecord; //总记录数
 - private int totalPage; //总页数
 - private Map<String, String> params; //查询条件
 - private Map<String, List<String>> paramLists; //数组查询条件
 - private String searchUrl; //Url地址
 - private String pageNoDisp; //可以显示的页号(分隔符"|",总页数变更时更新)
 - private Page() {
 - pageNo = 1;
 - pageSize = Integer.valueOf(DEFAULT_PAGESIZE);
 - totalRecord = 0;
 - totalPage = 0;
 - params = Maps.newHashMap();
 - paramLists = Maps.newHashMap();
 - searchUrl = "";
 - pageNoDisp = "";
 - }
 - public static Page newBuilder(int pageNo, int pageSize, String url){
 - Page page = new Page();
 - page.setPageNo(pageNo);
 - page.setPageSize(pageSize);
 - page.setSearchUrl(url);
 - return page;
 - }
 - /**
 - * 查询条件转JSON
 - */
 - public String getParaJson(){
 - Map<String, Object> map = Maps.newHashMap();
 - for (String key : params.keySet()){
 - if ( params.get(key) != null ){
 - map.put(key, params.get(key));
 - }
 - }
 - String json="";
 - try {
 - json = mapper.writeValueAsString(map);
 - } catch (Exception e) {
 - logger.error("转换JSON失败", params, e);
 - }
 - return json;
 - }
 - /**
 - * 数组查询条件转JSON
 - */
 - public String getParaListJson(){
 - Map<String, Object> map = Maps.newHashMap();
 - for (String key : paramLists.keySet()){
 - List<String> lists = paramLists.get(key);
 - if ( lists != null && lists.size()>0 ){
 - map.put(key, lists);
 - }
 - }
 - String json="";
 - try {
 - json = mapper.writeValueAsString(map);
 - } catch (Exception e) {
 - logger.error("转换JSON失败", params, e);
 - }
 - return json;
 - }
 - /**
 - * 总件数变化时,更新总页数并计算显示样式
 - */
 - private void refreshPage(){
 - //总页数计算
 - totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : (totalRecord/pageSize + 1);
 - //防止超出最末页(浏览途中数据被删除的情况)
 - if ( pageNo > totalPage && totalPage!=0){
 - pageNo = totalPage;
 - }
 - pageNoDisp = computeDisplayStyleAndPage();
 - }
 - /**
 - * 计算页号显示样式
 - * 这里实现以下的分页样式("[]"代表当前页号),可根据项目需求调整
 - * [1],2,3,4,5,6,7,8..12,13
 - * 1,2..5,6,[7],8,9..12,13
 - * 1,2..6,7,8,9,10,11,12,[13]
 - */
 - private String computeDisplayStyleAndPage(){
 - List<Integer> pageDisplays = Lists.newArrayList();
 - if ( totalPage <= 11 ){
 - for (int i=1; i<=totalPage; i++){
 - pageDisplays.add(i);
 - }
 - }else if ( pageNo < 7 ){
 - for (int i=1; i<=8; i++){
 - pageDisplays.add(i);
 - }
 - pageDisplays.add(0);// 0 表示 省略部分(下同)
 - pageDisplays.add(totalPage-1);
 - pageDisplays.add(totalPage);
 - }else if ( pageNo> totalPage-6 ){
 - pageDisplays.add(1);
 - pageDisplays.add(2);
 - pageDisplays.add(0);
 - for (int i=totalPage-7; i<=totalPage; i++){
 - pageDisplays.add(i);
 - }
 - }else{
 - pageDisplays.add(1);
 - pageDisplays.add(2);
 - pageDisplays.add(0);
 - for (int i=pageNo-2; i<=pageNo+2; i++){
 - pageDisplays.add(i);
 - }
 - pageDisplays.add(0);
 - pageDisplays.add(totalPage-1);
 - pageDisplays.add(totalPage);
 - }
 - return Joiner.on("|").join(pageDisplays.toArray());
 - }
 - public int getPageNo() {
 - return pageNo;
 - }
 - public void setPageNo(int pageNo) {
 - this.pageNo = pageNo;
 - }
 - public int getPageSize() {
 - return pageSize;
 - }
 - public void setPageSize(int pageSize) {
 - this.pageSize = pageSize;
 - }
 - public int getTotalRecord() {
 - return totalRecord;
 - }
 - public void setTotalRecord(int totalRecord) {
 - this.totalRecord = totalRecord;
 - refreshPage();
 - }
 - public int getTotalPage() {
 - return totalPage;
 - }
 - public void setTotalPage(int totalPage) {
 - this.totalPage = totalPage;
 - }
 - public Map<String, String> getParams() {
 - return params;
 - }
 - public void setParams(Map<String, String> params) {
 - this.params = params;
 - }
 - public Map<String, List<String>> getParamLists() {
 - return paramLists;
 - }
 - public void setParamLists(Map<String, List<String>> paramLists) {
 - this.paramLists = paramLists;
 - }
 - public String getSearchUrl() {
 - return searchUrl;
 - }
 - public void setSearchUrl(String searchUrl) {
 - this.searchUrl = searchUrl;
 - }
 - public String getPageNoDisp() {
 - return pageNoDisp;
 - }
 - public void setPageNoDisp(String pageNoDisp) {
 - this.pageNoDisp = pageNoDisp;
 - }
 - }
 
然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。
核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。
为简化代码使用了Commons JXPath 包,做对象查询。
- /**
 - * 分页用拦截器
 - */
 - import java.sql.Connection;
 - import java.sql.PreparedStatement;
 - import java.sql.ResultSet;
 - import java.util.Properties;
 - import org.apache.commons.jxpath.JXPathContext;
 - import org.apache.commons.jxpath.JXPathNotFoundException;
 - import org.apache.ibatis.executor.Executor;
 - import org.apache.ibatis.executor.parameter.DefaultParameterHandler;
 - import org.apache.ibatis.mapping.BoundSql;
 - import org.apache.ibatis.mapping.MappedStatement;
 - import org.apache.ibatis.mapping.MappedStatement.Builder;
 - import org.apache.ibatis.mapping.ParameterMapping;
 - import org.apache.ibatis.mapping.SqlSource;
 - 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.session.ResultHandler;
 - import org.apache.ibatis.session.RowBounds;
 - @Intercepts({@Signature(type=Executor.class,method="query",args={ MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })})
 - public class PageInterceptor implements Interceptor{
 - public Object intercept(Invocation invocation) throws Throwable {
 - //当前环境 MappedStatement,BoundSql,及sql取得
 - MappedStatement mappedStatement=(MappedStatement)invocation.getArgs()[0];
 - Object parameter = invocation.getArgs()[1];
 - BoundSql boundSql = mappedStatement.getBoundSql(parameter);
 - String originalSql = boundSql.getSql().trim();
 - Object parameterObject = boundSql.getParameterObject();
 - //Page对象获取,“信使”到达拦截器!
 - Page page = searchPageWithXpath(boundSql.getParameterObject(),".","page","*/page");
 - if(page!=null ){
 - //Page对象存在的场合,开始分页处理
 - String countSql = getCountSql(originalSql);
 - Connection connection=mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection() ;
 - PreparedStatement countStmt = connection.prepareStatement(countSql);
 - BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql);
 - DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, countBS);
 - parameterHandler.setParameters(countStmt);
 - ResultSet rs = countStmt.executeQuery();
 - int totpage=0;
 - if (rs.next()) {
 - totpage = rs.getInt(1);
 - }
 - rs.close();
 - countStmt.close();
 - connection.close();
 - //分页计算
 - page.setTotalRecord(totpage);
 - //对原始Sql追加limit
 - int offset = (page.getPageNo() - 1) * page.getPageSize();
 - StringBuffer sb = new StringBuffer();
 - sb.append(originalSql).append(" limit ").append(offset).append(",").append(page.getPageSize());
 - BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, sb.toString());
 - MappedStatement newMs = copyFromMappedStatement(mappedStatement,new BoundSqlSqlSource(newBoundSql));
 - invocation.getArgs()[0]= newMs;
 - }
 - return invocation.proceed();
 - }
 - /**
 - * 根据给定的xpath查询Page对象
 - */
 - private Page searchPageWithXpath(Object o,String... xpaths) {
 - JXPathContext context = JXPathContext.newContext(o);
 - Object result;
 - for(String xpath : xpaths){
 - try {
 - result = context.selectSingleNode(xpath);
 - } catch (JXPathNotFoundException e) {
 - continue;
 - }
 - if ( result instanceof Page ){
 - return (Page)result;
 - }
 - }
 - return null;
 - }
 - /**
 - * 复制MappedStatement对象
 - */
 - private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) {
 - Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType());
 - builder.resource(ms.getResource());
 - builder.fetchSize(ms.getFetchSize());
 - builder.statementType(ms.getStatementType());
 - builder.keyGenerator(ms.getKeyGenerator());
 - builder.keyProperty(ms.getKeyProperty());
 - builder.timeout(ms.getTimeout());
 - builder.parameterMap(ms.getParameterMap());
 - builder.resultMaps(ms.getResultMaps());
 - builder.resultSetType(ms.getResultSetType());
 - builder.cache(ms.getCache());
 - builder.flushCacheRequired(ms.isFlushCacheRequired());
 - builder.useCache(ms.isUseCache());
 - return builder.build();
 - }
 - /**
 - * 复制BoundSql对象
 - */
 - private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {
 - BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
 - for (ParameterMapping mapping : boundSql.getParameterMappings()) {
 - String prop = mapping.getProperty();
 - if (boundSql.hasAdditionalParameter(prop)) {
 - newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
 - }
 - }
 - return newBoundSql;
 - }
 - /**
 - * 根据原Sql语句获取对应的查询总记录数的Sql语句
 - */
 - private String getCountSql(String sql) {
 - return "SELECT COUNT(*) FROM (" + sql + ") aliasForPage";
 - }
 - public class BoundSqlSqlSource implements SqlSource {
 - BoundSql boundSql;
 - public BoundSqlSqlSource(BoundSql boundSql) {
 - this.boundSql = boundSql;
 - }
 - public BoundSql getBoundSql(Object parameterObject) {
 - return boundSql;
 - }
 - }
 - public Object plugin(Object arg0) {
 - return Plugin.wrap(arg0, this);
 - }
 - public void setProperties(Properties arg0) {
 - }
 - }
 
到展示层终于可以轻松些了,使用了文件标签来简化前台开发。
采用临时表单提交,CSS使用了Bootstrap。
- <%@tag pageEncoding="UTF-8"%>
 - <%@ attribute name="page" type="cn.com.intasect.ots.common.utils.Page" required="true"%>
 - <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 - <%
 - int current = page.getPageNo();
 - int begin = 1;
 - int end = page.getTotalPage();
 - request.setAttribute("current", current);
 - request.setAttribute("begin", begin);
 - request.setAttribute("end", end);
 - request.setAttribute("pList", page.getPageNoDisp());
 - %>
 - <script type="text/javascript">
 - var paras = '<%=page.getParaJson()%>';
 - var paraJson = eval('(' + paras + ')');
 - //将提交参数转换为JSON
 - var paraLists = '<%=page.getParaListJson()%>';
 - var paraListJson = eval('(' + paraLists + ')');
 - function pageClick( pNo ){
 - paraJson["pageNo"] = pNo;
 - paraJson["pageSize"] = "<%=page.getPageSize()%>";
 - var jsPost = function(action, values, valueLists) {
 - var id = Math.random();
 - document.write('<form id="post' + id + '" name="post'+ id +'" action="' + action + '" method="post">');
 - for (var key in values) {
 - document.write('<input type="hidden" name="' + key + '" value="' + values[key] + '" />');
 - }
 - for (var key2 in valueLists) {
 - for (var index in valueLists[key2]) {
 - document.write('<input type="hidden" name="' + key2 + '" value="' + valueLists[key2][index] + '" />');
 - }
 - }
 - document.write('</form>');
 - document.getElementById('post' + id).submit();
 - }
 - //发送POST
 - jsPost("<%=page.getSearchUrl()%>", paraJson, paraListJson);
 - }
 - </script>
 - <div class="page-pull-right">
 - <% if (current!=1 && end!=0){%>
 - <button class="btn btn-default btn-sm" onclick="pageClick(1)">首页</button>
 - <button class="btn btn-default btn-sm" onclick="pageClick(${current-1})">前页</button>
 - <%}else{%>
 - <button class="btn btn-default btn-sm" >首页</button>
 - <button class="btn btn-default btn-sm" >前页</button>
 - <%} %>
 - <c:forTokens items="${pList}" delims="|" var="pNo">
 - <c:choose>
 - <c:when test="${pNo == 0}">
 - <label style="font-size: 10px; width: 20px; text-align: center;">•••</label>
 - </c:when>
 - <c:when test="${pNo != current}">
 - <button class="btn btn-default btn-sm" onclick="pageClick(${pNo})">${pNo}</button>
 - </c:when>
 - <c:otherwise>
 - <button class="btn btn-primary btn-sm" style="font-weight:bold;">${pNo}</button>
 - </c:otherwise>
 - </c:choose>
 - </c:forTokens>
 - <% if (current<end && end!=0){%>
 - <button class="btn btn-default btn-sm" onclick="pageClick(${current+1})">后页</button>
 - <button class="btn btn-default btn-sm" onclick="pageClick(${end})">末页</button>
 - <%}else{%>
 - <button class="btn btn-default btn-sm">后页</button>
 - <button class="btn btn-default btn-sm">末页</button>
 - <%} %>
 - </div>
 
注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。
- page.getPageNo() //当前页号
 - page.getTotalPage() //总页数
 - page.getPageNoDisp() //可以显示的页号
 - page.getParaJson() //查询条件
 - page.getParaListJson() //数组查询条件
 - page.getPageSize() //每页行数
 - page.getSearchUrl() //Url地址(作为action名称)
 
到这里三个核心模块完成了。然后是拦截器的注册。
【拦截器的注册】
需要在mybatis-config.xml 中加入拦截器的配置
- <plugins>
 - <plugin interceptor="cn.com.dingding.common.utils.PageInterceptor">
 - </plugin>
 - </plugins>
 
【相关代码修改】
首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。
1)入参需增加 pageNo,pageSize 两个参数
2)根据pageNo,pageSize 及你的相对url构造page对象。(
3)最重要的是将你的其他入参(查询条件)保存到page中
4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)
5)将page对象传回Mode中
修改前
- @RequestMapping(value = "/user/users")
 - public String list(
 - @ModelAttribute("name") String name,
 - @ModelAttribute("levelId") String levelId,
 - @ModelAttribute("subjectId") String subjectId,
 - Model model) {
 - model.addAttribute("users",userService.selectByNameLevelSubject(
 - name, levelId, subjectId));
 - return USER_LIST_JSP;
 - }
 
修改后
- @RequestMapping(value = "/user/users")
 - public String list(
 - @RequestParam(required = false, defaultValue = "1") int pageNo,
 - @RequestParam(required = false, defaultValue = "5") int pageSize,
 - @ModelAttribute("name") String name,
 - @ModelAttribute("levelId") String levelId,
 - @ModelAttribute("subjectId") String subjectId,
 - Model model) {
 - // 这里是“信使”诞生之地,一出生就加载了很多重要信息!
 - Page page = Page.newBuilder(pageNo, pageSize, "users");
 - page.getParams().put("name", name); //这里再保存查询条件
 - page.getParams().put("levelId", levelId);
 - page.getParams().put("subjectId", subjectId);
 - model.addAttribute("users",userService.selectByNameLevelSubject(
 - name, levelId, subjectId, page));
 - model.addAttribute("page", page); //这里将page返回前台
 - return USER_LIST_JSP;
 - }
 
注意pageSize的缺省值决定该分页的每页数据行数 ,实际项目更通用的方式是使用配置文件指定。
Service层
拦截器可以自动识别在Map或Bean中的Page对象。
如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。
- @Override
 - public List<UserDTO> selectByNameLevelSubject(String name, String levelId, String subjectId, Page page) {
 - Map<String, Object> map = Maps.newHashMap();
 - levelId = DEFAULT_SELECTED.equals(levelId)?null: levelId;
 - subjectId = DEFAULT_SELECTED.equals(subjectId)?null: subjectId;
 - if (name != null && name.isEmpty()){
 - name = null;
 - }
 - map.put("name", name);
 - map.put("levelId", levelId);
 - map.put("subjectId", subjectId);
 - map.put("page", page); //MAP的话加这一句就OK
 - return userMapper.selectByNameLevelSubject(map);
 - }
 
前台页面方面,由于使用了标签,在适当的位置加一句就够了。
- <tags:page page="${page}"/>
 
“信使”page在这里进入标签,让分页按钮最终展现。
至此,无需修改一句sql,完成分页自动化。
【效果图】

【总结】
现在回过头来看下最开始提出的几个问题:
1)分页时是要随时带有最近一次查询条件
回答:在改造Controller层时,通过将提交参数设置到 Page对象的 Map<String, String> params(单个基本型参数) 和 Map<String, List<String>> paramLists(数组基本型)解决。
顺便提一下,例子中没有涉及参数是Bean的情况,实际应用中应该比较常见。简单的方法是将Bean转换层Map后加入到params。
2)不能影响现有的sql,类似aop的效果
回答:利用Mybatis提供了 Interceptor 接口,拦截后改头换面去的件数并计算limit值,自然能神不知鬼不觉。
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
回答:@Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) 只拦截查询语句,其他增删改查不会影响。
4)尽量少的影响现有service等接口
回答:这个自认为本方案做的还不够好,主要是Controller层改造上,感觉代码量还比较大。如果有有识者知道更好的方案还请多指教。
【遗留问题】
1)一个“明显”的性能问题,是每次检索前都要去 select count(*)一次。在很多时候(数据变化不是特别敏感的场景)是不必要的。调整也不难,先Controller参数增加一个 totalRecord 总记录数 ,在稍加修改一下Page相关代码即可。
2)要排序怎么办?本文并未讨论排序,但是方法是类似的。以上面代码为基础,可以较容易地实现一个通用的排序标签。
===================================== 分割线 (1/8)=======================================
对于Controller层需要将入参传入Page对象的问题已经进行了改善,思路是自动从HttpServletRequest 类中提取入残,减低了分页代码的侵入性,详细参看文章 http://duanhengbin.iteye.com/blog/2001142
===================================== 分割线 (1/23)=======================================
再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。
===================================== 分割线 (2/21)=======================================
今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:
一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):
- <pager url="/user/users" pagesize="10" />
 
二是增加一个过滤器,并将剩下的位于Control类中 唯一侵入性的分页相关代码移入过滤器。发现当前的 Url 在配置文件中有匹配是就构造Page对象,并加入到Response中。
使用最终版后,对于开发者需要分页时,只要在配置文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需要任何改动,可以说简化到了极限。
【技术列表】
总结下最终方案用到的技术:
- Mybatis 提供的拦截器接口实现(实现分页sql自动 select count 及limit 拼接)
 - Servlet过滤器+ThreadLocal (生成线程共享的Page对象)
 - 标签文件 (实现前端共通的分页效果)
 - 临时表单提交 (减少页面体积)
 
【其他分页方案比较】
时下比较成熟的 JPA 的分页方案,(主要应用在 Hibernate + Spring Data 的场合),主要切入点在DAO层,而Controller等各层接口依然需要带着pageNumber,pageSize 这些的参数,另外框架开发者还要掌握一些必须的辅助类,如:
org.springframework.data.repository.PagingAndSortingRepository 可分页DAO基类
org.springframework.data.domain.Page 抽取结果封装类
org.springframework.data.domain.Pageable 分页信息类
比较来看 本方案 做到了分页与业务逻辑的完全解耦,开发者无需关注分页,全部通过配置实现。通过这个例子也可以反映出Mybatis在底层开发上有其独特的优势。
【备选方案】
最后再闲扯下,上面的最终案是基于 Url 配置的,其实也可以基于方法加自定义注解来做。这样配置文件省了,但是要增加一个注解解析类。注解中参数 为初始的每页行数。估计注解fans会喜欢,如下面的样子:
- @RequestMapping(value = "/user/users")
 - @Pagination(size=10)
 - public String list(
 - ...
 
同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。
详细参考源码来源minglisoft.cn/technology
分享一个完整的Mybatis分页解决方案的更多相关文章
- 很好用的mybatis分页解决方案
		
分页如果写在SQL脚本中,将会大大影响我们后续数据库的迁移难度.mybatis的分页一般是自己实现一个mybatis的拦截器,然后根据某些特定的条件开启分页,对原有SQL进行改造. 正在我对mybat ...
 - 一个完整的mybatis项目,包含增删改查
		
1.导入jar包,导入相关配置文件,均在自己博客园的文件中 编写mybatis.xml文件 <?xml version="1.0" encoding="UTF-8& ...
 - 【Android】 分享一个完整的项目,适合新手!
		
写这个app之前是因为看了头条的一篇文章:http://www.managershare.com/post/155110,然后心想要不做一个这样的app,让手机计算就行了.也就没多想就去开始整了. ...
 - MongoDB + Spark: 完整的大数据解决方案
		
Spark介绍 按照官方的定义,Spark 是一个通用,快速,适用于大规模数据的处理引擎. 通用性:我们可以使用Spark SQL来执行常规分析, Spark Streaming 来流数据处理, 以及 ...
 - 分享一个手机端好用的jquery ajax分页类
		
分享一个手机端好用的jquery ajax分页类 jquery-ias.min.js 1,引入jquery-ias.min.js 2,调用ajax分页 <script type="te ...
 - 分享一个.NET(C#)按指定字母个数截断英文字符串的方法–提供枚举选项,可保留完整单词
		
分享一个.NET(C#)按字母个数截断英文字符串的方法,该方法提供枚举选项.枚举选项包括:可保留完整单词,允许最后一个单词超过最大长度限制,字符串最后跟省略号以及不采取任何操作等,具体示例实现代码如下 ...
 - mybatis分页的一种解决方案
		
mybatis自定义分页解决方案 1.PageSqlProvider<T> —— 提供默认的分页列表查询 package com.xinyartech.erp.core.base; im ...
 - 分享一个漂亮的ASP.NET MVC界面框架
		
本文分享一个插件化的界面框架,该框架提供了用户.角色.权限管理功能,也提供了插件的管理和插件中心.下图是该界面框架的样式(全部源码和原理介绍下一篇分享,推荐越多,源码放的越早,呵呵). 要使用该界面框 ...
 - SSM 使用 mybatis 分页插件 pagehepler 实现分页
		
使用分页插件的原因,简化了sql代码的写法,实现较好的物理分页,比写一段完整的分页sql代码,也能减少了误差性. Mybatis分页插件 demo 项目地址:https://gitee.com/fre ...
 
随机推荐
- 大数据量场景下storm自定义分组与Hbase预分区完美结合大幅度节省内存空间
			
前言:在系统中向hbase中插入数据时,常常通过设置region的预分区来防止大数据量插入的热点问题,提高数据插入的效率,同时可以减少当数据猛增时由于Region split带来的资源消耗.大量的预分 ...
 - 给自己的 MAC 添加一个桌面日历
			
使用 Ubuntu 做自己的办公环境用了将近三年,最近换了新款的 MBP,系统都用的很舒服. 不过 Ubuntu 是在我的 TP W540上部署的,而 W540 + 电源适配太重了(我的电池是9芯的) ...
 - 老李推荐:第5章1节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 官方简介
			
老李推荐:第5章1节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 官方简介 在MonkeyRunner的框架中,Monkey是作为一个服务来接受来自Monkey ...
 - vuejs学习笔记(1)--属性,事件绑定,ajax
			
属性 v-for 类似于angular中的 ng-repeat ,用于重复生成html片段: <ul id="box"> <li v-for="(v, ...
 - Grafana中多租户设置
			
Grafana中通过设置不同的组织,以及将用户分配到不同组织,来做到多租户,类似门户的概念. Grafana默认是不允许非管理员用户创建新的组织的,这个可以通过修改配置文件以允许非管理员用户创建组织: ...
 - `define、parameter、localparam三者的区别
			
`define: 可以跨模块的定义,写在模块名称上面,在整个设计工程都有效.一旦'define指令被编译,其在整个编译过程中都有效.例如,通过另一个文件中的`define指令,定义的常量可以被其他文件 ...
 - Python多层目录模块调用
			
一. 引用模块在 父+级目录中: 1. 将导入模块所在目录(../model/模块)添加到系统环境变量path下,可添加多个 import syssys.path.append("../mo ...
 - .net 图片压缩
			
压缩图片方法: /// <summary> /// 生成缩略图 /// </summary> /// <param name="originalImagePat ...
 - R语言写2048游戏
			
2048 是一款益智游戏,只需要用方向键让两两相同的数字碰撞就会诞生一个翻倍的数字,初始数字由 2 或者 4 构成,直到游戏界面全部被填满,游戏结束. 编程时并未查看原作者代码,不喜勿喷. 程序结构如 ...
 - Linux - 函数的栈帧
			
栈帧(stack frame),机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储.为单个过程(函数调用)分配的那部分栈称为栈帧.栈帧其实是两个指针寄存器, 寄存器%ebp为帧 ...