一、场景


    公司新开发的一个web项目,项目中一个功能是从失败交易流水表中按日期查询失败的交易,以列表的形式展示出来。前端列表使用了DataTable,DataTable自带前端分页和后端分页。所谓前端分页就是一次性从数据库中查出所有数据返回给前端,前端自动进行分页。这种处理方式在数据量较小的情况下还可以,当数据量较大(具体数据量没有测试)会导致前端加载数据缓慢卡顿,同时因为后端一次性从数据库查出大量数据放在内存中,会导致内存资源消耗过大而卡顿甚至宕机。为了解决这个瓶颈问题,采用后端分页的形式。后端分页即前端传递当前页码以及当前页显示的数据量给后端,后端只查询当前页要进行展示的数据。从而避开了由于数据量过大而导致的卡顿问题。


二、基础代码


    DataTable默认的分页机制为前端分页,此处不过多陈述,通过查找资料了解到,后端分页需要将原来的


bServerSide:false

修改为:

bServerSide:true

后端分页对返回的JSON数据格式有要求,具体格式如下:

{"sEcho":"1","iTotalRecords":"0","iTotalDisplayRecords":"0","aaData":[]}

其中sEcho是前端传递的,只需获取然后原数返回即可,iTotalRecords字面理解意思是当前数据表中总的记录数,iTotalDisplayRecords是当前页面要展示的记录数,aaData为返回列表的数据。


    这里先展示一版基础代码,也就是我从网上查找资料写的代码,通过基础代码,后边一步一步的发现问题解决问题。


HTML部分:


</div>
       <div class="mr-20 ml-20">
           <table id="trafficMonitorFailed" class="table table-border table-bordered table-hover table-bg table-sort">
                   <thead>
                       <tr class="text-c">
                           <th>交易日期</th>
                           <th>交易时间</th>
                           <th>交易流水号</th>
                           <th>交易唯一标识</th>
                           <th>渠道</th>
                           <th>交易响应时间</th>
                           <th>交易分类</th>
                           <th>交易编码</th>
                           <th>交易描述</th>
                           <th>错误码</th>
                           <th>错误描述</th>
                       </tr>
                   </thead>
               </table>
        </div>

JS部分:

function dataTableDraw(){
 $("#trafficMonitorFailed").dataTable({
  bServerSide:true, //开启后端分页
  bDestroy: true,         //下边两个属性应该是重载数据相关的 不加在加载数据会弹窗报错 点击确定后显示数据
  bRetrieve: true,
  bProcessing: true, //显示加载数据时的提示
  bInfo:true,  //显示信息 如 当前x页 共x条数据等
  bSort:true,  //允许排序
  bFilter:true,  //检索、筛选框
  sAjaxSource: rootUrl + "getTrafficMonitorFailedList", //请求url
  bLengthChange:true, //支持变更页面显示数据行数
  sPaginationType: "bootstrap", //翻页风格
  bPaginate:true,  //显示翻页按钮
  fnServerData: retrieveData, //执行函数
  aoColumns:[//列表元素  支持多种属性 
                   { "mData": "tranDate","fnRender":function(data,val){
        var JSONDate = new Date(val.time); 
        return JSONDate.format('yyyy-MM-dd');
       },"width":"200px"},
                   { "mData": "tranTime","fnRender":function(data,val){
        var JSONTime = new Date(val.time);
        return JSONTime.format('yyyy-MM-dd HH:mm:ss');
       }},
                   { "mData": "seqNo"},
                   { "mData": "platformId"},
                   { "mData": "channel"},
                   { "mData": "costTime"},
                   { "mData": "reqType"},
                   { "mData": "reqInterface"},
                   { "mData": "reqDesc","bSortable":false},  //不允许当前页排序
                   { "mData": "retCode","bSortable":false},
                   { "mData": "retMsg","bSortable":false}
               ],
      oLanguage: {  
       "sProcessing" : "正在加载中......",  
       "sLengthMenu" : "_MENU_",  
       "sZeroRecords" : "无记录",  
       "sEmptyTable" : "表中无数据存在!",  
       "sInfo" : "当前显示 _START_ 到 _END_ 条,共 _MAX_  条记录",  
       "sInfoEmpty" : "没有数据",  
       "sInfoFiltered" : "数据表中共为 _TOTAL_ 条记录",  
       "sSearch" : " ",  
       "oPaginate" : {  
        "sFirst" : " 首页 ",  
        "sPrevious" : " 上一页 ",  
        "sNext" : " 下一页 ",  
        "sLast" : " 末页 "  
        }  
 }  
 });
 $(".dataTables_wrapper .dataTables_filter input").attr("placeholder","检索内容");
}
//对应上边的回调函数 参数个数不变 名字可改 第一个为请求url  第二个为上送数据 第三个为回调函数
function retrieveData(sSource,aoData,fnCallback) {
 var startDate = {
   "name":"startDate",
   "value":$("#from").val()
 }
 var endDate = {
   "name":"endDate",
   "value":$("#to").val()
 }
 //我这里按照请求数据的格式增加了自己的查询条件 请求数据格式固定为 name-value的格式 可以使用
 //alert打印查看 包含了基本的页码、页面数据元素、等信息以及新增的查询条件
 aoData.push(startDate);
 aoData.push(endDate);
 $.ajax({
     url : sSource,//这个就是请求地址对应sAjaxSource
     data : {"aoData":JSON.stringify(aoData)},//这个是把datatable的一些基本数据传给后台,比如起始位置,每页显示的行数
     type : 'post',
     dataType : 'json',
     async : false,
     success : function(result) {
         fnCallback(result);//把返回的数据传给这个方法就可以了,datatable会自动绑定数据的
     },
     error : function(msg) {
     }
 });
}


后端Controller层:



@RequestMapping(value="/page/getTrafficMonitorFailedList",method=RequestMethod.POST)
	@ResponseBody
	public String getTrafficMonitorFailedList(String aoData){
		List<TrafficMonitorFailed> trafficMonitorFailed = new ArrayList<TrafficMonitorFailed>();
        JSONArray jsonarray=(JSONArray) JSONArray.fromObject(aoData);//json格式化用的是fastjson
        if(jsonarray == null||jsonarray.size() ==0){
           return WebFactory.createErrorResponse("错误的查询条件");
        }
        String startDate = formatDate.format(new Date());
        String endDate = formatDate.format(new Date());
        String sEcho = null;
        int iDisplayStart = 0; // 起始索引
        int iDisplayLength = 10; // 每页显示的行数
        int count = 0;
        for (int i = 0; i < jsonarray.size(); i++) {
            JSONObject obj = (JSONObject) jsonarray.get(i);
            if (obj.get("name").equals("sEcho"))
                sEcho = obj.get("value").toString();

            if (obj.get("name").equals("iDisplayStart"))
                iDisplayStart =Integer.parseInt(obj.get("value").toString());

            if (obj.get("name").equals("iDisplayLength"))
                iDisplayLength = Integer.parseInt(obj.get("value").toString());
            
            if (obj.get("name").equals("startDate"))
            	startDate = obj.get("value").toString();
            
            if (obj.get("name").equals("endDate"))
            	endDate = obj.get("value").toString();
            
        }
        
            trafficMonitorFailed = this.trafficMonitorFailedService.getTrafficMonitorFailedListSearch(startDate, endDate, iDisplayStart, iDisplayLength);
            count = this.trafficMonitorFailedService.getTrafficMonitorFailedListCountSearch(startDate, endDate).getTotal();
        
        JSONObject getObj = new JSONObject();
        getObj.put("sEcho", sEcho);// DataTable前台必须要的
        getObj.put("iTotalRecords",count);
        getObj.put("iTotalDisplayRecords",trafficMonitorFailed.size());
        getObj.put("aaData", trafficMonitorFailed);//把查到数据装入aaData,要以JSON格式返回
        return getObj.toString();

	}




第一版的基础代码如上,其中有一些如数据Model的代码,不同业务场景Model不同,此处不做列举。


三、问题描述/解决

Q1.Cannot read property 'length' of undefined

    上述代码运行之后,页面加载之后一直显示加载中,F12到调试模式可以看到控制台报错“Cannot read property 'length of undefined'”,而且这个是jquery报的错,可以说是非常恼火了。经过分析,可能是返回的数据格式不对,因此在js的回调函数中进行了修改,将JSON字符串转换成JSON数据的格式,修改后如下:

$.ajax({
     url : sSource,//这个就是请求地址对应sAjaxSource
     data : {"aoData":JSON.stringify(aoData)},//这个是把datatable的一些基本数据传给后台,比如起始位置,每页显示的行数
     type : 'post',
     dataType : 'json',
     async : false,
     success : function(result) {
    	 var ret = eval("(" + result + ")");
         fnCallback(ret);//把返回的数据传给这个方法就可以了,datatable会自动绑定数据的
     },
     error : function(msg) {
     }
 });

修改之后运行,此问题解决。

Q2.DataTables warning (table id = 'trafficMonitorFailed'):Requested unknown parameter '0' from the data source for row 0


    上述代码运行之后,页面弹窗报错“DataTables warning (table id ='trafficMonitorFailed'):Requested unknown parameter '0' from the data source for row 0”这个问题诈一看也是一脸蒙蔽,后来发现应该是dataTable版本的问题,需要将数据表中的mData修改为mDataProp,修改之后如下:


{ "mDataProp": "tranDate","fnRender":function(data,val){
					   var JSONDate = new Date(val.time);	
					   return JSONDate.format('yyyy-MM-dd');
				   },"width":"200px"},
                   { "mDataProp": "tranTime","fnRender":function(data,val){
					   var JSONTime = new Date(val.time);
					   return JSONTime.format('yyyy-MM-dd HH:mm:ss');
				   }},
                   { "mDataProp": "seqNo"},
                   { "mDataProp": "platformId"},
                   { "mDataProp": "channel"},
                   { "mDataProp": "costTime"},
                   { "mDataProp": "reqType"},
                   { "mDataProp": "reqInterface"},
                   { "mDataProp": "reqDesc","bSortable":false},
                   { "mDataProp": "retCode","bSortable":false},
                   { "mDataProp": "retMsg","bSortable":false}

修改之后运行,问题解决。

Q3.数据明明很多,DataTable仅显示了一页的数据,并且没有显示其它页的按钮,不可以翻页。


    这个问题的意思是,我数据库中有很多数据,F12调试也能看到返回了一页10条数据,并且iTotalRecords为32,按道理应该显示一页数据之后,可以进行翻页,总共显示4页,但是实际的页面却只显示了一页数据,列表下方的翻页处也仅仅有“1”的页签,没有其它的页签,上一页下一页都不能点击。这个问题困扰了我许久,后来私信网上一位大神给出了解决方案。原因是iTotalRecords和iTotalDisplayRecords数据放反了。这个我到现在也没有理解,按照字面意思itotalRecords确实是应该放总查询数据量,iTotalDisplayRecords为当前页要展示的数据量。而实际的使用过程中,这两个数据应该是反了过来。不知道其它人有没有遇到这种情况。


修改代码如下:


getObj.put("iTotalRecords",trafficMonitorFailed.size());
getObj.put("iTotalDisplayRecords",count);


即将原来两个放置数据统计个数的值互换。运行之后,问题解决,心心念念的后端分页终于实现了。


Q4.后端分页实现之后,检索筛选、排序功能失效。


    实现了基本的后端分页,点击了几页试了一下,分页效果没问题,但是检索跟排序的功能都没有反应。于是F12开启调试模式,发现点击排序和在检索框输入文字之后,都发起了后台请求,因此可以判定,后端分页的检索跟排序功能需要后端代码实现。实现原理,在前端传递的aoData中,会储存要排序的列、排序的方式以及检索的内容。因此可以通过后端修改SQL的形式完成。


修改之后的Controller代码为:


@RequestMapping(value="/page/getTrafficMonitorFailedList",method=RequestMethod.POST)
	@ResponseBody
	public String getTrafficMonitorFailedList(String aoData){
		List<TrafficMonitorFailed> trafficMonitorFailed = new ArrayList<TrafficMonitorFailed>();
        JSONArray jsonarray=(JSONArray) JSONArray.fromObject(aoData);//json格式化用的是fastjson
        if(jsonarray == null||jsonarray.size() ==0){
           return WebFactory.createErrorResponse("错误的查询条件");
        }
        String startDate = formatDate.format(new Date());
        String endDate = formatDate.format(new Date());
        String sEcho = null;
        int iDisplayStart = 0; // 起始索引
        int iDisplayLength = 10; // 每页显示的行数
        int orderColumn = 0;//默认排序列
        String orderDir = "asc";//默认排序方式为升序
        String sSearch = "";//默认检索内容
        int count = 0;
        for (int i = 0; i < jsonarray.size(); i++) {
            JSONObject obj = (JSONObject) jsonarray.get(i);
            if (obj.get("name").equals("sEcho"))
                sEcho = obj.get("value").toString();

            if (obj.get("name").equals("iDisplayStart"))
                iDisplayStart =Integer.parseInt(obj.get("value").toString());

            if (obj.get("name").equals("iDisplayLength"))
                iDisplayLength = Integer.parseInt(obj.get("value").toString());
            
            if (obj.get("name").equals("startDate"))
            	startDate = obj.get("value").toString();
            
            if (obj.get("name").equals("endDate"))
            	endDate = obj.get("value").toString();
            
            if (obj.get("name").equals("iSortCol_0"))
            	orderColumn = Integer.parseInt(obj.get("value").toString());
            
            if (obj.get("name").equals("sSortDir_0"))
            	orderDir = obj.get("value").toString();
            
            if (obj.get("name").equals("sSearch"))
            	sSearch = obj.get("value").toString();
        }
        
        if("".equals(sSearch)||sSearch == null){
            trafficMonitorFailed = this.trafficMonitorFailedService.getTrafficMonitorFailedList(startDate, endDate, iDisplayStart, iDisplayLength, orderColumn, orderDir);
            count = this.trafficMonitorFailedService.getTrafficMonitorFailedListCount(startDate, endDate).getTotal();
        }else{
            trafficMonitorFailed = this.trafficMonitorFailedService.getTrafficMonitorFailedListSearch(startDate, endDate, iDisplayStart, iDisplayLength, orderColumn, orderDir,sSearch);
            count = this.trafficMonitorFailedService.getTrafficMonitorFailedListCountSearch(startDate, endDate,sSearch).getTotal();
        }
        JSONObject getObj = new JSONObject();
        getObj.put("sEcho", sEcho);// DataTable前台必须要的
        getObj.put("iTotalRecords",trafficMonitorFailed.size());//显示的行数,这个要和上面写的一样
        getObj.put("iTotalDisplayRecords",count);//总行数
        getObj.put("aaData", trafficMonitorFailed);//把查到数据装入aaData,要以JSON格式返回
        return getObj.toString();

	}

这里前端传递的orderColumn是int类型,标记的是列的序号,SQL语句中可以判断序号然后ORDER BY指定的列,orderDir传递的是排序方式有两种一种是ASC另一种是DESC。sSearch传递的是检索框的内容,这里为了避免没有检索的情况还使用like语句模糊查找而影响效率,没有想到其它办法,单单的使用了判断sSearch如果为空则不使用like语句查找。多说一句,分页查询使用的sql语句我这里使用了limit a,b的形式,从a+1位置查,查b条数据。这里忘了贴SQL语句,大概的形式就是select xxx from xxx where xxx order by xxx asc/desc limit a,b 这种,如果后期数据量大了出现瓶颈再继续优化。

Q5.给DataTable增加横向滚动条

    为了美观,我想保证每一条数据都在一行上显示,不进行换行。然后就会出现数据过长,一页显示不下的情况,因此需要增加滚动条。网上查到不同版本使用的形式不同,我这里是使用如下形式

sScrollX:"100%"

还有另一种方式是将“100%”改为true的形式。增加滚动条还需要设置文本框的数据处于一行显示而不是自动换行。


修改html代码如下:


</div>
       <div class="mr-20 ml-20">
           <table id="trafficMonitorFailed" class="table table-border table-bordered table-hover table-bg table-sort" style="white-space:nowrap">
                   <thead>
                       <tr class="text-c">
                        	<th>交易日期</th>
                           <th>交易时间</th>
                           <th>交易流水号</th>
                           <th>交易唯一标识</th>
                           <th>渠道</th>
                           <th>交易响应时间</th>
                           <th>交易分类</th>
                           <th>交易编码</th>
                           <th>交易描述</th>
                           <th>错误码</th>
                           <th>错误描述</th>
                       </tr>
                   </thead>
               </table>
        </div>

即增加了样式:

style="white-space:nowrap"

    至此,后端分页的功能全部实现。如有不足,还望指正。