- 浏览: 1034697 次
- 性别:
- 来自: 上海
文章分类
- 全部博客 (1441)
- 软件思想&演讲 (9)
- 行业常识 (250)
- 时时疑问 (5)
- java/guava/python/php/ruby/R/scala/groovy (213)
- struct/spring/springmvc (37)
- mybatis/hibernate/JPA (10)
- mysql/oracle/sqlserver/db2/mongdb/redis/neo4j/GreenPlum/Teradata/hsqldb/Derby/sakila (268)
- js/jquery/jqueryUi/jqueryEaseyUI/extjs/angulrJs/react/es6/grunt/zepto/raphael (81)
- ZMQ/RabbitMQ/ActiveMQ/JMS/kafka (17)
- lucene/solr/nuth/elasticsearch/MG4J (167)
- html/css/ionic/nodejs/bootstrap (19)
- Linux/shell/centos (56)
- cvs/svn/git/sourceTree/gradle/ant/maven/mantis/docker/Kubernetes (26)
- sonatype nexus (1)
- tomcat/jetty/netty/jboss (9)
- 工具 (17)
- ETL/SPASS/MATLAB/RapidMiner/weka/kettle/DataX/Kylin (11)
- hadoop/spark/Hbase/Hive/pig/Zookeeper/HAWQ/cloudera/Impala/Oozie (190)
- ios/swift/android (9)
- 机器学习&算法&大数据 (18)
- Mesos是Apache下的开源分布式资源管理框架 (1)
- echarts/d3/highCharts/tableau (1)
- 行业技能图谱 (1)
- 大数据可视化 (2)
- tornado/ansible/twisted (2)
- Nagios/Cacti/Zabbix (0)
- eclipse/intellijIDEA/webstorm (5)
- cvs/svn/git/sourceTree/gradle/jira/bitbucket (4)
- jsp/jsf/flex/ZKoss (0)
- 测试技术 (2)
- splunk/flunm (2)
- 高并发/大数据量 (1)
- freemarker/vector/thymeleaf (1)
- docker/Kubernetes (2)
- dubbo/ESB/dubboX/wso2 (2)
最新评论
Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。
我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,新年第一天,特此发文回馈网站。
特别鸣谢 paginator项目 (https://github.com/miemiedev/mybatis-paginator ) ,阅读源码帮助很大。
【背景】
项目框架是 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类对象会以“信使”的身份出现在全部与分页相关的地方。
Java代码 收藏代码
/**
* 封装分页数据
*/
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 包,做对象查询。
Java代码 收藏代码
/**
* 分页用拦截器
*/
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。
Html代码 收藏代码
<%@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方法全部用上了。
Java代码 收藏代码
page.getPageNo() //当前页号
page.getTotalPage() //总页数
page.getPageNoDisp() //可以显示的页号
page.getParaJson() //查询条件
page.getParaListJson() //数组查询条件
page.getPageSize() //每页行数
page.getSearchUrl() //Url地址(作为action名称)
到这里三个核心模块完成了。然后是拦截器的注册。
【拦截器的注册】
需要在mybatis-config.xml 中加入拦截器的配置
Java代码 收藏代码
<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中
修改前
Java代码 收藏代码
@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;
}
修改后
Java代码 收藏代码
@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则比较简单,以下是例子。
Java代码 收藏代码
@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);
}
前台页面方面,由于使用了标签,在适当的位置加一句就够了。
Html代码 收藏代码
<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/blogs/2001142
===================================== 分割线 (1/23)=======================================
再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。
===================================== 分割线 (2/21)=======================================
今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:
一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):
Xml代码 收藏代码
<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会喜欢,如下面的样子:
Java代码 收藏代码
@RequestMapping(value = "/user/users")
@Pagination(size=10)
public String list(
...
同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。
我在最近的项目中开发了这个通用分页器,过程中参考了站内不少好文章,新年第一天,特此发文回馈网站。
特别鸣谢 paginator项目 (https://github.com/miemiedev/mybatis-paginator ) ,阅读源码帮助很大。
【背景】
项目框架是 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类对象会以“信使”的身份出现在全部与分页相关的地方。
Java代码 收藏代码
/**
* 封装分页数据
*/
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 包,做对象查询。
Java代码 收藏代码
/**
* 分页用拦截器
*/
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。
Html代码 收藏代码
<%@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方法全部用上了。
Java代码 收藏代码
page.getPageNo() //当前页号
page.getTotalPage() //总页数
page.getPageNoDisp() //可以显示的页号
page.getParaJson() //查询条件
page.getParaListJson() //数组查询条件
page.getPageSize() //每页行数
page.getSearchUrl() //Url地址(作为action名称)
到这里三个核心模块完成了。然后是拦截器的注册。
【拦截器的注册】
需要在mybatis-config.xml 中加入拦截器的配置
Java代码 收藏代码
<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中
修改前
Java代码 收藏代码
@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;
}
修改后
Java代码 收藏代码
@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则比较简单,以下是例子。
Java代码 收藏代码
@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);
}
前台页面方面,由于使用了标签,在适当的位置加一句就够了。
Html代码 收藏代码
<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/blogs/2001142
===================================== 分割线 (1/23)=======================================
再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。
===================================== 分割线 (2/21)=======================================
今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:
一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):
Xml代码 收藏代码
<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会喜欢,如下面的样子:
Java代码 收藏代码
@RequestMapping(value = "/user/users")
@Pagination(size=10)
public String list(
...
同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。
发表评论
-
mybatis的学习
2016-01-06 10:18 547首先,POJO /** * @Title: ... -
Hibernate 的Criteria
2017-01-01 23:34 4581、Criteria Hibernate 设计 ... -
hibernate的延时加载的get和load的区别
2017-01-01 23:34 291在hibernate中我们知道如果要从数据库中得到一个对象,通 ... -
hibernate的拦截器(Interceptor)
2017-01-01 23:34 1460拦截器(Interceptor) org.hibe ... -
hibernate原理
2015-12-15 15:06 4431.Hibernate是如何连接数据库 主要是 ... -
mybatis的配置config文件
2015-12-06 22:24 1027MyBatis是一个支持普通SQL查询,存储过程和高级映射的优 ... -
mybatis的配置文件
2015-12-06 21:58 677只为成功找方法,不为失败找借口! MyBatis学习总结(三 ... -
Mybatis-分页
2015-12-02 02:33 697Mybatis的分页功能很弱,它是基于内存的分页(查出所有记录 ... -
hibernatet特点
2015-11-23 00:38 313Hibernate优点 (1) 对象/关系数据库映射(ORM) ...
相关推荐
Mybatis PageHelper是一款针对Mybatis框架的高效分页插件,它可以帮助开发者在进行数据库查询时轻松实现分页效果,从而提升用户体验并优化系统性能。PageHelper插件由开源中国社区的free团队开发,可以在其官方Git...
本压缩包“超强php分页打包 通用分页 万能分页 ajax分页 google分页”提供了一系列的分页解决方案,包括了基本的PHP分页、通用的分页实现、以及结合AJAX技术的动态分页,旨在满足各种项目需求。下面将详细介绍这些...
本教程将深入讲解如何利用RecyclerView实现横向和纵向的分页滑动,充分利用Android原生功能。 首先,理解RecyclerView的基本结构。RecyclerView包含一个Adapter,它负责将数据集绑定到视图上,以及一个...
在网页开发中,分页是一种常见的用户界面设计,用于处理大量数据时的高效展示。Java AJAX(Asynchronous JavaScript and XML)分页与JSP(JavaServer Pages)相结合,可以提供无需刷新整个页面即可动态加载更多内容...
在网页开发中,分页是不可或缺的一个功能,特别是在数据量庞大的时候,为了提高用户体验,将大量数据分成多个页面展示,而不是一次性加载所有内容。本文将详细介绍一个基于jQuery的分页插件,它具有代码简洁、兼容性...
本篇将深入探讨JavaScript实现分页的相关知识点,包括前台分页、客户端分页、分页机制以及相关的JavaScript代码示例。 首先,我们要理解什么是分页。在Web应用中,分页是将大量数据分成多个小部分(每部分通常称为...
高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页高级分页...
### 数据库分页技术详解 #### 一、引言 在大数据时代,高效的数据管理和查询技术变得尤为重要。其中,数据库分页技术作为一种提高用户体验、优化数据检索效率的重要手段,在各类应用系统尤其是Web应用中得到了广泛...
在Windows Presentation Foundation (WPF) 中,开发人员经常需要处理大量的数据展示,此时分页功能就显得尤为重要。WPF DataGrid 控件是一个强大的表格展示工具,它可以方便地展示和操作数据,而添加分页功能则可以...
在IT领域,分页和递归是两种常见的数据处理技术,尤其在大数据量的Web应用中,它们扮演着至关重要的角色。"分页+递归显示分页+递归显示"这一主题涉及到如何有效地管理和展示大量层级数据。下面将详细阐述这两个概念...
为了解决这个问题,我们可以实现分页功能,让数据分批次加载,从而提高界面响应速度。本文将深入探讨如何在`QTableWidget`和`QTableView`中实现分页栏。 首先,我们需要理解`QTableWidget`和`QTableView`的区别。`...
java实现分页 jsp分页 分页java实现分页 jsp分页 分页java实现分页 jsp分页 分页java实现分页 jsp分页 分页java实现分页 jsp分页 分页java实现分页 jsp分页 分页
分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码分页代码...
JavaScript(简称JS)分页效果是网页开发中常见的功能,尤其在处理大量数据时,用于提高用户体验和加载速度。此项目中的"JS分页效果"指的是利用JavaScript和相关的库(如jQuery)实现对数据进行分页显示的技术。下面...
jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页jsp 分页...
2.1.目前网络上很多分页主键,不能进行按钮选择, 2.2.甚至对样式修改的难度也较大, 2.3.使用在开发过程中由于使用到了分页功能,在这里写了一个分页功能。 3.按钮选择 之前的很多按钮是通过参数进行选择,这样不...
在MySQL数据库中,分页是处理大量数据查询时不可或缺的一种技术。它允许用户按需加载数据,而不是一次性获取所有记录,从而提高了用户体验并降低了服务器负载。以下是对分页实现的详细说明: 一、基础概念 分页是将...
在ASP中实现分页功能是常见的需求,特别是在处理大量数据列表时,分页能够提高用户体验,避免一次性加载过多内容导致页面加载缓慢。下面将详细探讨ASP分页的相关知识点。 一、分页的基本原理 分页主要是通过计算总...
AspNetPager除提供默认的类似于DataGrid和GridView的PostBack分页方式外,还支持通过Url进行分页,象大多数asp程序中分页一样, Url分页方式允许用户通过在浏览器地址栏中输入相应的地址即可直接进入指定页面,也...
**jQuery分页插件及其应用** 在Web开发中,数据量较大的时候,为了提高用户体验,通常会采用分页的方式来展示信息。jQuery分页插件因其轻量级、易用性而受到开发者们的广泛喜爱。本资源包含“最简单分页”至“50种...