高级查询功能是针对所有流程及表单数据的查询入口,作为工作流报表的暂时性替代功能,对于领导和相关流程的业务单位来说十分重要。高级查询功能提供了包括了流程名称、发起人、发起人所属部门等的基本条件查询以及可用作查询条件(是否可用作查询条件在列定义中设置,选中某个表单后,可供选择的列自动展示,并且根据列定义中的数据格式提供不同的查询方式,比如模糊匹配、比如日期的区间查询等)的高级条件查询,通过组合查询条件得到想要的查询结果。
由于高级查询一直存在分布的问题,究其原因主要是前端的展示与后台的分布难以协同,oaGrid或者原生的kendoGrid均无法展现动态列(这个问题网上确实没有例子,有gridView可以实现动态列的范例)。高级查询先后经历无分页(用<table>标签拼接)、伪前端分页等一系列演化。
高级查询的权限控制等在前面的博文中有介绍,本文主要介绍针对高级查询结果的前后端协同的分页实现。下面介绍一下目前的实现逻辑以及整个优化的历程。该功能的需求最初由我定义,形成需求后下发给工程师R具体实现,关于条件组合、sql拼接我们暂时不介绍,当时作为雏形的结果展示是一种一次查询出所有结果,然后采用<table>标签动态接拼展示动态的查询结果。下面是最初的实现:
/** * 动态创建table * */ function createDyTable(showColumn, columnName, resultList) { //组装table表头 var showColumns = showColumn; var title = "<tr>"; var value = ""; $('#dyTable').empty(); if(columnName.length > 0 && showColumn.length > 0) { $.each(columnName, function(index, items) { title += "<th>" + items + "</th>"; }); title += "<th>操作</th>"; title += "</tr>"; $('#dyTable').append(title); $.each(resultList, function(idx, key) { value = "<tr>"; $.each(showColumn, function(index, items) { var rowValue = key['' + "f_" + items + '']; value += "<td style='word-break:break-all;width:300px;'>" + rowValue + "</td>"; }); var id = key['f_id']; var operation = "<td style='word-break:break-all;width:200px;'>"; operation += "<span class='operate_detail'></span><a onclick=\"querySeniorDetailById(\'" + id + "\');return false;\" href=\"javascript:void(0)\">查看详情</a> "; operation += "<span class='operate_history'></span><a onclick=\"querySeniorHistoryById(\'" + id + "\');return false;\" href=\"javascript:void(0)\">查看历史</a> "; operation += "</td>"; value += operation; value += "</tr>"; $('#dyTable').append(value); }); } };
以上的js比较简单,第一、从高级查询条件中获得表单信息;第二、循环遍历一行一行地写进<table>。
由于开发这一版本的时候正是OA一期上线的关键时期,R对我说实现分布的功能对于kendoGrid是不可能的,而当时我们所有的表格的展示用的都是基于kendoGrid的oaGrid,熟悉kendoGrid和js的主管Z也说kendoGrid实现动态列的分布是不行的。当时刚刚担任主管的我本着“评价个冰箱,自己一定也得会制冷”、“要求别人做到的,自己先做到”,而当时的我刚从上家公司C/S开发上转过来不久,对BS结构以及js等前端的开发技术并不了解,所以只有暂时接受这种根本没有分页的方案。
但这个一直是我关注的心结之一,不久我将该任务分配给了高中毕业,但热衷技术、态度端正、经总监推荐、总经理还是副总裁特批进来的L,结果仍然是以grid无法实现动态列且前后端分页而不了了之。
再接下来,新入职的G,熟悉js但对grid并不了解。我当时按照固有的思路,即实现前后端分页是不行的。在当时的OA的原项目经理、后来转做技术总监的徐总指导下,在这个固有的思路指引下,开发了基于原生kendoGrid的页面的前端分页。该实现并不是真正的分页查询,只是将所有数据查询后,在前端的分页展示。但这个方案以及其部分实现确实为我后来解决oaGrid的分页提供了一种很好的思路。我们看一下这个阶段的实现:
function newCreateDyTable(showColumn, columnName, resultList) { $("#dyTable").html(""); var provinces = "" //组装table表头 var showColumns = showColumn; if(columnName.length > 0 && showColumn.length > 0) { var clumnss = []; $.each(columnName, function(index, items) { clumnss.push({ field: showColumn[index], title: items, width: 150 }); }); clumnss.push({ title: "流程状态", field:"f_lczt", width : 150 }); var opertion = [] clumnss.push({ title: "操作", field:"opertion", template: function(id) { var operation = ""; operation += "<span class='operate_detail'></span><a onclick=\"querySeniorDetailById(\'" + id.opertion + "\');return false;\" href=\"javascript:void(0)\">查看详情</a> "; operation += "<span class='operate_history'></span><a onclick=\"querySeniorHistoryById(\'" + id.opertion + "\');return false;\" href=\"javascript:void(0)\">查看历史</a> "; return operation; }, width : 150 }); $.each(resultList, function(idx, key) { provinces += "{ "; $.each(showColumn, function(index, items) { var rowValue = key['' + "f_" + items + '']; provinces+="\""+items+"\""+":"+"\""+rowValue+"\","; }); if(""!=provinces&&null!=provinces){ var id = key['f_id']; var f_lczt = key['f_lczt']; provinces+= "\"f_lczt\""+":"+"\"" +f_lczt + "\","; provinces+= "\"opertion\""+":"+"\"" + id + "\""; } provinces += " }," }); } if(""!=provinces&&null!=provinces){ provinces = provinces.substring(0, provinces.length-1); } provinces = "[" + provinces+"]"; var reg = /(\r\n)/g; provinces.replace(reg, " ") var dateobj = eval('(' + provinces + ')'); $("#dyTable").kendoGrid({ dataSource: { data: dateobj, pageSize: 10 }, selModel:true, pageable: { input: true, numeric: false, messages: { display: "{0} - {1} 共 {2} 条数据", empty: "没有数据", page: "页", of: "/ {0}", itemsPerPage: "条每页", first: "第一页", previous: "前一页", next: "下一页", last: "最后一页", refresh: "刷新" } }, columns: clumnss }); }
这一版本的相校初始版本一是实现了用户直接感观的分页,二是表格样式比较美观。
这一版本的设计中实际已经组装出了oaGrid所需的columns,其设计思路剑走偏锋,对columns的组装从查询的高级条件中直接获取,比我之前设想的从后台组装columns更简单、易行且更符合需求(从后台组装实际无法判断哪些字段是用户期望展示的,虽然可以将状态\id等写死在判断代码中)。
通过第二版本引出了关于前台应用oaGrid分页的思路,前端的代码如下,这一版本相当简单,保持了代码风格的统一,即查询结果使用oaGrid展示及分页:
$('#myToDoTaskGrid').oaGrid({ beforeload: function(operation) { var processDefineVo = {dataMap:{ seniorQueryInfo:seniorQueryInfo, showColumn:showColumn },seniorQueryBaseDto:seniorQueryBaseDto}; operation.processDefineVo = processDefineVo; }, title:'待办任务查询结果', selModel:true, url : '../workflow/querySeniorInfo.action', columns : clumnss });
参考一般模块的grid分页的逻辑,其grid的列是固定的,分页逻辑的采用了com.gaochao.platform.orm.mybatis.PagingIntercept,对所有的query方法进行分页,如传进的参数有start和limit,就将start和limit设置进来,如果没有,则默认使用limit=2147483647,offset=0,为普通查询语句添加分页逻辑。在oaGrid.js中有page\skip用来传递页码和页数,然后进行查询。
经过查看gc.oaTools.js中的oaGird实现,在第L608
schema : { model : dataModel, data : 'pageResult.result' , total : 'pageResult.count'
}并结合网上的关于dendoGrid的范例model的格式即为我们使用一般模块使用oaGrid时前端传入的columns数组,再反观data和total即是我们com.gaochao.platform.entity.PageResult中的两个属性,由此我们得到一种方案,即columns不由前端传入而改为重写PageResult加入dataModel,并在后端实现dataModel的拼接(拼接会查询数据库,可能影响效率)。
第二种方案:很多关于mybatis的分页方案是采用了所谓的Interceptor,对某些数据库操作进行拦截然后处理,我们的数据范围就是使用的com.gaochao.oa.module.com.dependence.server.service.impl.DataruleHandler,这是一种职责链的设计模式。网上提供的插件,也就是interceptor方案我们的系统里已有,即是对所有一般使用oaGrid的后端统一的分页处理:com.gaochao.platform.orm.mybatis.PagingIntercept,代码如下:
@Intercepts({@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) }) public class PagingIntercept implements Interceptor { private PagingDialect pagingDialect; public Object intercept(Invocation invocation) throws Throwable { //query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; RowBounds rowBounds = (RowBounds) args[2]; int offset = rowBounds.getOffset(); int limit = rowBounds.getLimit(); if (pagingDialect.supportsPaging() && (offset != RowBounds.NO_ROW_OFFSET || limit != RowBounds.NO_ROW_LIMIT)) { BoundSql boundSql = ms.getBoundSql(parameter); String sql = boundSql.getSql().trim(); sql = pagingDialect.getPagingSql(sql, offset, limit); offset = RowBounds.NO_ROW_OFFSET; limit = RowBounds.NO_ROW_LIMIT; args[2] = new RowBounds(offset, limit); BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); copyMetaParameters(boundSql, newBoundSql); MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql)); args[0] = newMs; } return invocation.proceed(); } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { String className = (String) properties.get("dialect"); if(StringUtils.isBlank(className)) { throw new MyBatisException("分页方言 不能为空."); } Class<?> dialectClass = null; try { dialectClass = ClassUtils.forName(className); } catch (Exception e) { throw new MyBatisException(e); } pagingDialect = (PagingDialect) BeanUtils.instantiate(dialectClass); } /** * 复制BoundSql的MetaParameter * <br>------------------------------<br> * @param lhs * @param rhs */ private void copyMetaParameters(BoundSql lhs, BoundSql rhs) { for (ParameterMapping map : lhs.getParameterMappings()) { String key = map.getProperty(); Object value = lhs.getAdditionalParameter(key); if (null != value) { rhs.setAdditionalParameter(key, value); } } } // see: MapperBuilderAssistant private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) { Builder builder = new MappedStatement.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()); // setStatementTimeout() builder.timeout(ms.getTimeout()); // setStatementResultMap() builder.parameterMap(ms.getParameterMap()); // setStatementResultMap() builder.resultMaps(ms.getResultMaps()); builder.resultSetType(ms.getResultSetType()); // setStatementCache() builder.cache(ms.getCache()); builder.flushCacheRequired(ms.isFlushCacheRequired()); builder.useCache(ms.isUseCache()); return builder.build(); } public static class BoundSqlSqlSource implements SqlSource { private BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) { this.boundSql = boundSql; } public BoundSql getBoundSql(Object parameterObject) { return boundSql; } } }
如前所述,当我们有start、limit时,该方法使用start\limit进行分页,如果没有传入,相当于不分页;该拦截针对的所有query方法
@Intercepts({@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
我们的高级查询也是已有拦截的,只是因为我们没有传入start\limit,默认不分页;另外G在做前端伪分页时采用了拼接columns的方法将columns传入,但在接下来的处理是查询所有,然后在前端使用原生的kendogrid进行类似table的动态拼接即newCreateDyTable方法。这种方法是将结果一次查出,分页展示。结合PagingIntercept和G的columns拼接方法,我们只需要简单修改高级查询的查询逻辑,传入start、limit等分页元素,前端完全复用一般模块的gird。这种分页是后端的分页,且不用增加太多的代码。可以肯定,在同样的sql和数据量的情况下,这种分页要比不分页或前端分页给用户的体验更佳,等待时间更短。