以前在搞.net的时候,咱们能够借助强大的ExpressionTree来解决,以前有一篇是微软的EntityFramework表达式转换:Linq to Entity经验:表达式转换,是将一种表达式转换成数据库组件可以识别的表达式,只不过那篇没有涉及到View中的条件而已。页面动态查询的最简单的方法就是解析View中特定的值来获得后台组件可以识别的查询逻辑。
咱们期待View中可以这样指定条件:
前端
<input type="text" name="WHERE.storeName.LIKE" class="form-control" style="width: 180px;" " />
它的意思是查询字段storeName,操做符是like,看起来并不难,但要解决这么几个问题:mysql
参数收集问题,表单域的值以什么样的方式提交到后台?angularjs
后台接收参数类型是什么?web
如何将表单域中的条件转换成数据库组件可以识别的条件?ajax
咱们选择的数据库组件是mybatis+mysql。我的感受mybatis在处理动态查询时比JPA在前期(技术学习前期,即水平还不太够的时候)要简单些,也多是我对JPA的认识还不够,总感受mybatis这种拼SQL的方式比较熟悉一些,也比较容易控制。固然它们的定位自己就不一样,这里很少讨论。基于mybatis咱们采用了tk.mybatis这个开源的组件,它的功能很是丰富,分页,通用mapper,代码生成等大部分功能都已经包含,你们有兴趣能够去搜索。正则表达式
注:下面的功能是个人同事完成,这里我作为学习的过程来分享下,可能也有理解不到位的地方,纯属我的学习理解。其中有部分功能未展现出来(好比权限过滤,and or这些分组查询的支持等),只包含最基本的,每一个项目的需求不一样以及团队环境不一样能够会有多种实现方式,选择你们都能接受的就能够了。
咱们再分别看下上面的三个问题怎么解决:spring
参数收集问题,表单域的值以什么样的方式提交到后台方法?sql
通常作页面查询时请求数据就两种方式,get或者post。get通常是在采用了ajax这类技术,post就复杂一些,分为两种:一种也是采用ajax提交到后台,一种是表单的提交。这里呢,因为咱们采用了angularjs,因此很显然只能采用ajax提交,若是查询条件多,可采用ajax的post。因为上面贴的代码片断显示条件的name是动态的,因此咱们不可能定义一个具体的后台的业务Model对前台的条件,好比有name,email,phone等等,因此咱们采用将表单域整个序列化后的结果传递到后台。数据库
var requestData = $("#"+options.searchFormId+"").serialize(); var url = listUrl+"?"+requestData+"&pageNum="+$scopeLocal.pageRequest.pageNum; $.ajax({ type : "POST", url : url, dataType : 'json', async : false, beforeSend:options.beforeSend, error:options.error, success : function(data) { $scopeLocal.pageResponse = data; $scopeLocal.content=data.list; options.callback($scopeLocal,data); } });
2:参数类型是什么?
若是是表单提交方式,咱们能够采用HttpServletRequest这个对象来接收全部表单域的值,但上一步咱们采用的提交方式并不是表单自身的提交,而是ajax的提交,ajax的请求,是不识别HttpServletRequest这个参数类型的,为此咱们须要定义一个自定义的公共的对象来接收咱们动态View中指定的条件,这里就有个咱们的SearchModel,它包含以下内容:json
分页信息,当前页,页数据大小
搜索条件信息集合List<SearchFilter>,一堆咱们本身定义的条件,主要包含字段名称,操做符以及值,这里是本文的重点。
转换为SQL的逻辑
SearchFilter:
public final class SearchFilter implements Serializable { private String propertyName; private Object value; private Operator operator; private String orGroup;
SearchModel:
public class SearchModel implements Serializable { private List<SearchFilter> searchFilters; private int pageNum = 1; private int pageSize = 10;
上面搜索条件的信息,咱们须要从View中获取,这里应用HandlerMethodArgumentResolver来解决,它只有两个方法:
判断是不是支持的参数类型
boolean supportsParameter(MethodParameter parameter);
这个方法实现比较简单,只须要判断下当前的参数类型是不是指定的类型便可:
@Override public boolean supportsParameter(MethodParameter parameter) { Class<?> parameterType = parameter.getParameterType(); return SearchModel.class.isAssignableFrom(parameterType); }
解析数据的详细过程
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
这个方法是核心,以前有提到过,由于咱们是ajax提交,在后台的controller方法中不能包含HttpServletRequest request 这个参数,但后台要想取表单域的值从哪取呢?其实仍是从这个参数中取,只不过咱们须要换一种方法,能够从上面接口的的webRequest对象中获取,有了这个对象也就意味着你获得了表单域的全部值了,后台的事情就好办了。
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
下面只须要一个转换类将HttpServletRequest中的表单值填充到咱们自定义的SearchModel中就能够了,这里须要一个专业处理转换有类SearchFilterBuilder,首先将表单值转换成一个集合,字符串类型的:
public static SearchFilterBuilder from(final HttpServletRequest request) { return new SearchFilterBuilder(request); } public List<String> buildToStrings() { return buildToStrings(true); } public List<String> buildToStrings(final boolean containsDataAuth) { List<String> searchFilterStrings = Lists.newArrayList(); Map<String, String[]> map = request.getParameterMap(); for (Map.Entry<String, String[]> entry : map.entrySet()) { String strKey = entry.getKey(); for (String value : entry.getValue()) { if (!Strings.isNullOrEmpty(value) && !"none".equals(value) && strKey.startsWith(preWhere)) { String filedAndOp = strKey.substring(preWhere.length()); searchFilterStrings.add(String.format("%s=%s", filedAndOp, value)); } } } if (containsDataAuth) { // to do } return searchFilterStrings; }
基于上面获得的条件集合进一步解析条件,因为咱们前端View传递的条件是字符串的,因此这里应用了一个专门的正则表达式的类DefaultSearchFilterStringProcessor去解析数据
public List<SearchFilter> build() { List<String> searchFilterStrings = buildToStrings(); List<SearchFilter> searchFilters = Lists.newArrayList(); searchFilters.addAll(searchFilterStrings.stream() .map(DefaultSearchFilterStringProcessor::from) .collect(Collectors.toList())); return searchFilters; }
因为字符串处理的逻辑与本文关联并不大,这里就不贴相关代码了,不会正则的用最笨的人肉解析字符串也是能够的,如前面说的都拿到Request对象了后面都好操做。
3:若是将表单域中的条件转换成数据库组件可以识别的条件?
tk.mybatis或者是官方的mybatis-spring组件都支持动态条件,因为我这采用的是tk.mybatis,因此某些类都是tk.mybatis的,tk是进一步的封装,因此原理大致是相同的,以前有提到过操做mybtis有点像操做原生SQL,感受就是一种拼SQL的过程,这里咱们拼这个动态条件也是相似,简单的话只须要一个静态转换方法就能够了,若是再深刻一点能够想办法作成自动识别并转换,有能力的可研究。无非就是以下的转换:
switch (op) { case EQ: this.criteria.andEqualTo(filed, value); break; case NOTEQ: this.criteria.andNotEqualTo(filed, value); break; case LE: this.criteria.andLessThanOrEqualTo(filed, value); break;
解决完上面这些,咱们就能够直接这样写后台代码了:
controller:
@RequestMapping(value = "/getAllByPage") @ResponseBody public PageInfo<BcStore> getAllByPage(final SearchModel s1) { this.convertSearchModel(s1); return storeService.select(s1); }
service:有了example对象,分页信息,排序字段,后面的就是tk.mybatis的基本功能了。
@Override public final PageInfo<T> select(final SearchModel searchModel) { Example example = ExampleBuilder.forClass(genericType).fromSearchFilters(searchModel.getSearchFilters()).build(); return select(example, searchModel.getPageNum(), searchModel.getPageSize(), searchModel.getOrderBy()); }
基于上面的内容,针对查询条件,咱们能够在View中任意指定查语语句所包含的条件,后台的controller以及service基本保持不变,应付普通的管理界面查询足够了。