以前在搞.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基本保持不變,應付普通的管理界面查詢足夠了。