主題
公司在basecode的用法上是比較有新意的,所以准備記錄分享下公司的用法.
說明
basecode公司的一個主要用途就是用於一些基礎的代碼表,參數表的前台操作.這些表有很多,用spring data jpa(hibernate)的時候,又不想為每個表寫個實體,因為太多了,每個實體還需要dao和service等等..這些表用法很單一,主要用於前台表單的下拉框,比如證件種類有:身份證,軍官證,學生證....這些證件的代碼和名稱等信息就是存在一個代碼表里的.前台下拉的時候可能會列出所有值,也可能會有一些過濾.
這種用法下需要前后台配合,前台我們使用的是miniui,需要對miniui現有的組件進行一些封裝來實現自動查詢數據庫,后台也需要有對應的查詢的邏輯..這里主要介紹后台的一些封裝.
用途
主要用於前台下拉框自動填充數據庫里不同參數表,代碼表的代碼的參數的值(比如中國行政區划地區代碼和對應的代碼值),開發過程中不需要自己手寫JS或者后台代碼,全部都有封裝.只需要配置一下即可實現前台下拉框填充后台數據庫里的代碼表的值.
主要實現原理
1.配置表:因為沒有實體去對應代碼表或者參數表,所以肯定要有個配置表去記錄有哪些代碼表,哪些參數表,應該怎么去不同的表中根據何種邏輯去取值.這張表我們簡稱def表吧.
2.代碼邏輯:有了配置表以后我們相當於有了規則,知道根據配置的規則應該如何去找數據.那么還需要有一套代碼邏輯去實現這個規則.這套代碼邏輯其實不復雜.就是利用先自己根據不同情況拼接不同的SQL,然后通過JPA的執行原生SQL的方法去查詢各個配置的表的數據.
具體實現
配置表
字段很多,說明配置還是比較靈活的.不過大部分情況下大部分字段是用不到的.我介紹一下最主要的用法.
code_type:像ID一樣,為了找到一條配置
value_field:表明下拉框的顯示的值對應的代碼的對應SQL查出來的哪一列
name_field:表明下拉框的顯示的值對應SQL查出來的哪一列
value_field和name_field就是前台下拉框組件里顯示值和傳給后台的參數值對應數據庫里的SQL查詢結果的哪一列.比如下拉框顯示值'有效'對應的代碼是'Y'.或者說就是前台下拉框的key和value.
parent_field和isLeaf_field和enabled_field用於構建樹組件.樹組件和普通下拉框相比主要多了上級節點屬性(parent_field)和當前節點是否有下級節點(isLeaf_field)屬性.
Table_name就是要查哪張表的表名,或者直接寫SQL查詢也一樣.
FilterSQL就是前台傳給后台一些參數的情況下根據參數動態在Table_name后拼接where條件,達到過濾查詢結果的目的.
code_provider和bussinessdata_provider就是不走一般的查詢邏輯,自己定義比較復雜的查詢邏輯的時候用到.相當於一般情況下會有個默認的provider,你不用他默認的查詢邏輯,就需要自己配置.
實現
光有配置沒有什么用,我們來看看主要實現吧.
首先,既然我們把這么多不同業務場景的相同需求抽象出來做了一個統一處理,那我們肯定要有個統一入口.這個入口Controller我們就叫他basecode controller好了.
前面配置表中提到一條配置相當於是一種查數據的方式,對應一種下拉框取值的方式.那問我這個下拉框要怎么取值,相當於是問我配置表中怎么確定一條配置數據咯.
配置表中的主鍵,codeType能確定一條數據,所以每個下拉框向后台發送請求查找數據的時候肯定要把這個codeType傳過啦.(下拉框這個請求是在頁面加載的時候就會發起的,是公司封裝了miniui的組件,這篇文章主要涉及后台basecode,所以前台的封裝就暫時不說了.后面會再寫文章分享的).
controller中其實也沒做什么操作,就只是組裝一下和codeType同時傳過來的,用於數據過濾的參數,放到paramMap里和codeType一起傳給codeHelper,委托codeHelper去做具體的查詢與過濾數據.
1 @Override 2 public List<?> getCodeList(String codeType, Map<String, ?> paramMap) { 3 List<BaseCodeDTO> result = null; 4 try { 5 BaseCodeDefDTO defDto = codeDefService.getCodeDefByCodeType(codeType); 6 if (defDto != null) { 7 result = this.getProvider(defDto).getCodeList(defDto, paramMap); 8 } 9 } catch (Exception ex) { 10 LOGGER.info(ex.getMessage(), ex); 11 throw new RuntimeException(ex);// NOSONAR 12 } 13 return result; 14 }
從getCodeList方法可以看出,第5行先得到這個def表對應的java對象(jpa的映射方法).然后L7得到def表配置的Porvider(codeProvider成員域).再委托Provider去查找數據.所以說到底還是通過Provider去查找數據.
我們先來看看怎么getProvider的.
1 private UnifiedCodeProvider getProvider(BaseCodeDefDTO defDto) { 2 String providerDef = StringUtils.isNotEmpty(defDto.getCodeProvider()) ? defDto.getCodeProvider() 3 : "defaultCodeProvider"; 4 Object provider = SpringContextUtil.getBean(providerDef); 5 if (provider instanceof UnifiedCodeProvider) { 6 provider = (UnifiedCodeProvider) provider; 7 } else { 8 provider = (UnifiedCodeProvider) SpringContextUtil.getBean("defaultCodeProvider"); 9 } 10 if (provider == null) { 11 // TODO:異常拋出修改 12 throw new RuntimeException("basecode provider:" + defDto.getProvider() + " is not exist! ");// NOSONAR 13 } 14 return (UnifiedCodeProvider) provider; 15 16 }
從上面的代碼中可以看出,如果在def里配置了provider,那就從Spring的context中根據bean名字取出provider,同時也說明配置的provider是需要在項目啟動的時候由Spring加載的.
如果沒配置,那就用公共的provider,這個provider的名字叫做defaultCodeProvider.
取到provider以后我們就可以getCodeList了.我們主要看看公共的provider吧,因為絕大多數情況下用的是這個provider.
1 @Override 2 public List<BaseCodeDTO> getCodeList(BaseCodeDefDTO defDto, Map<String, ?> paramMap) { 3 // 重組參數 4 Map<String, Object> realMap = new HashMap<String, Object>(); 5 if (paramMap != null && !paramMap.isEmpty()) { 6 for (Entry<String, ?> entry : paramMap.entrySet()) { 7 if ("basevalue".equalsIgnoreCase(entry.getKey())) { 8 realMap.put(defDto.getParentField(), entry.getValue()); 9 } else if ("value".equalsIgnoreCase(entry.getKey())) { 10 realMap.put(defDto.getValueField(), entry.getValue()); 11 } else if ("label".equalsIgnoreCase(entry.getKey())) { 12 realMap.put(defDto.getNameField(), entry.getValue()); 13 } else if ("isleaf".equalsIgnoreCase(entry.getKey())) { 14 realMap.put(defDto.getIsleafField(), entry.getValue()); 15 } else if ("swjg".equalsIgnoreCase(entry.getKey())) { 16 realMap.put(defDto.getSwjgField(), entry.getValue()); 17 } else if ("enabled".equalsIgnoreCase(entry.getKey())) { 18 realMap.put(defDto.getEnabledField(), entry.getValue()); 19 } else if ("parent".equalsIgnoreCase(entry.getKey())) { 20 realMap.put(defDto.getParentField(), entry.getValue()); 21 } else { 22 realMap.put(entry.getKey(), entry.getValue()); 23 } 24 } 25 } 26 return codeService.getCodeListParamMap(defDto, realMap); 27 }
getCodeList里對傳過來的Map里的參數進行了一些加工,如果map里有key是"value"呀,"label"呀,"parent"呀類似這些值的話會被替換成def里配置的字符串,相當於是修改了map里的key,替換成了數據庫里配置的值,除此之外的key原封不動的放進新的map里.
然后委托codeService去查數據
1 public List<BaseCodeDTO> getCodeListParamMap(BaseCodeDefDTO codeModel, Map<String, Object> param) { 2 List<BaseCodeDTO> result = null; 3 try { 4 String baseSql = this.buildBaseSql(codeModel, false); 5 List<Object> paramList = new ArrayList<Object>(); 6 String whereCause = this.buildWhereCauseFromMap(codeModel, param, paramList); 7 if (param == null) { 8 param = new HashMap<String, Object>();// NOSONAR 9 } 10 StringBuilder sqlbuffer = new StringBuilder(); 11 sqlbuffer.append(baseSql).append(" WHERE ").append(whereCause).append(" ") 12 .append(StringUtils.isNotEmpty(codeModel.getOrderby()) ? codeModel.getOrderby() : ""); 13 Map<String, Object> paramMap = new HashMap<String, Object>(); 14 if (param != null && !param.isEmpty()) { 15 for (Entry<String, Object> entry : param.entrySet()) { 16 if (!"filterParam".equalsIgnoreCase(entry.getKey()) 17 && !"initParam".equalsIgnoreCase(entry.getKey())) { 18 paramMap.put(entry.getKey(), entry.getValue()); 19 } 20 } 21 } 22 result = navtiveRepository.executeQuerySql(sqlbuffer.toString(), paramMap, BaseCodeDTO.class); 23 } catch (Exception ex) { 24 LOGGER.info(ex.getMessage(), ex); 25 throw new RuntimeException(ex);// NOSONAR 26 } 27 return result; 28 29 }
getCodeListParamMap方法有點長,但是核心就是4行代碼:
L4:先拼接出要查的代碼表或者參數表的基本的查詢SQL,這里是通過調用buildBaseSql方法做到的.
L6:根據傳過來的Map里的參數拼接得到where條件字符串,這里是通過調用buildWhereCauseFromMap方法.
L11:L4+L6得到完整SQL語句,如果有配置order by的話,同時拼接order by.
L22:調用公司對JPA原生SQL的封裝的類來執行SQL(之前分享過這部分封裝).
1 private String buildBaseSql(BaseCodeDefDTO codeModel, boolean distinckSign) { 2 StringBuilder sqlBuffer = new StringBuilder(); 3 if (codeModel.getValueField() == null) { 4 throw new RuntimeException("代碼模型未設置值字段CodeField");// NOSONAR 5 } 6 if (codeModel.getNameField() == null) { 7 throw new RuntimeException("代碼模型未設置名稱字段NameField");// NOSONAR 8 } 9 if (codeModel.getTableName() == null) { 10 throw new RuntimeException("代碼模型未設置TABLE");// NOSONAR 11 } 12 13 sqlBuffer.append(" SELECT "); 14 if (distinckSign) { 15 sqlBuffer.append(" distinct "); 16 } 17 sqlBuffer.append(codeModel.getValueField()).append(" as value ,").append(codeModel.getNameField()); 18 sqlBuffer.append(" as label ,").append(codeModel.getParentField() != null ? codeModel.getParentField() : "''"); 19 sqlBuffer.append(" as parent, "); 20 sqlBuffer.append(codeModel.getEnabledField() != null ? codeModel.getEnabledField() : "''"); 21 sqlBuffer.append(" as enabled, "); 22 sqlBuffer.append(codeModel.getIsleafField() != null ? codeModel.getIsleafField() : "'false'"); 23 sqlBuffer.append(" as isleaf, ").append(codeModel.getSwjgField() != null ? codeModel.getSwjgField() : "''"); 24 sqlBuffer.append(" as swjgdm ").append(" from( ").append(codeModel.getTableName()).append(" ) t "); 25 return sqlBuffer.toString(); 26 27 }
buildBaseSql方法就是根據def表里的table_name列構造出了一個select語句.這個select語句select的內容是統一的.都是select value,label,parent等內容.因為前端下拉框組件需要的就是這么幾個內容,所以這里做到了統一處理.
同時def表里table列可以配置SQL或者Table name的原因在於這里是在table列的值會被當做子查詢(L24),所以寫SQL或者直接寫表名都是可以的.
1 @SuppressWarnings("unchecked") 2 private String buildWhereCauseFromMap(BaseCodeDefDTO codeModel, Map<String, Object> param, List<Object> paramList) 3 throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { 4 StringBuilder sqlBuffer = new StringBuilder(" 1=1 "); 5 if (param != null && param.size() > 0) { 6 Map<String, Object> scenarioParamMap = new HashMap<String, Object>(); 7 for (Entry<String, Object> entry : param.entrySet()) { 8 Object value = entry.getValue(); 9 if (value != null) { 10 if ("filterParam".equalsIgnoreCase(entry.getKey()) || "initParam".equalsIgnoreCase(entry.getKey())) { 11 if (StringUtils.isNotEmpty(codeModel.getFiltersql())) { 12 sqlBuffer.append(" AND "); 13 try { 14 Map<String, Object> map = (Map<String, Object>) value; 15 boolean hasMultipleScenarios = map.containsKey("SCENARIO"); 16 if (hasMultipleScenarios) { 17 Map<String, String> dataMap = new HashMap<String, String>(); 18 dataMap.put("SCENARIO", (String) map.get("SCENARIO")); 19 map.remove("SCENARIO"); 20 StringWriter sw = new StringWriter(); 21 processor.processPlainText(codeModel.getFiltersql(), dataMap, sw); 22 sqlBuffer.append(sw.toString()); 23 } else { 24 sqlBuffer.append(codeModel.getFiltersql()); 25 } 26 scenarioParamMap.putAll(map); 27 } catch (Exception e) { 28 LOGGER.info(e.getMessage(), e); 29 throw new RuntimeException(e);// NOSONAR 30 } 31 } 32 } else { 33 sqlBuffer.append(" AND "); 34 sqlBuffer.append(entry.getKey()).append(" = :" + entry.getKey()); 35 paramList.add(value); 36 } 37 } 38 } 39 param.putAll(scenarioParamMap); 40 } 41 return sqlBuffer.toString(); 42 43 }
buildWhereCauseFromMap方法略復雜.
除去各種校驗以外核心代碼為:
1 if ("filterParam".equalsIgnoreCase(entry.getKey()) || "initParam".equalsIgnoreCase(entry.getKey())) { 2 if (StringUtils.isNotEmpty(codeModel.getFiltersql())) { 3 sqlBuffer.append(" AND "); 4 try { 5 Map<String, Object> map = (Map<String, Object>) value; 6 boolean hasMultipleScenarios = map.containsKey("SCENARIO"); 7 if (hasMultipleScenarios) { 8 Map<String, String> dataMap = new HashMap<String, String>(); 9 dataMap.put("SCENARIO", (String) map.get("SCENARIO")); 10 map.remove("SCENARIO"); 11 StringWriter sw = new StringWriter(); 12 processor.processPlainText(codeModel.getFiltersql(), dataMap, sw); 13 sqlBuffer.append(sw.toString()); 14 } else { 15 sqlBuffer.append(codeModel.getFiltersql()); 16 } 17 scenarioParamMap.putAll(map); 18 } catch (Exception e) { 19 LOGGER.info(e.getMessage(), e); 20 throw new RuntimeException(e);// NOSONAR 21 } 22 } 23 } else { 24 sqlBuffer.append(" AND "); 25 sqlBuffer.append(entry.getKey()).append(" = :" + entry.getKey()); 26 paramList.add(value); 27 }
里面有2條分支:
1.L23 else路線:如果前面param的map里沒有filterParam或者initParam的key的話就直接把map里和key作為參數綁定拼接到SQL上,比如yxbz=:yxbz,后面jpa執行原生SQL的時候會把:yxbz替換成具體的值.
2.L1 filterParam和initParam路線:
這里就比較復雜了.
所謂initParam就是組件第一次加載的時候需要顯示的數據需要用到的參數.后續組件再發起請求的時候不會再傳這個參數.
所謂filterParam就是組件每次加載的時候需都要用到的參數.
initParam這種用法經常用在樹組件上,比如第一次加載樹組件的時候可能需要N個過濾條件,所以需要在initParam里傳遞N個過濾的參數,后續加載只需要加載你點擊的那個父節點下的子節點就行了這個時候initParam就什么參數都不用傳遞了.再傳遞一個parant參數表示父節點的值就行了.所以可能2次傳過來的參數是不同的.
而filterParam是每次都會傳的.
如果走這條路線的話def表里肯定會配置filterSQL,即where里要拼接的過濾的SQL.
filterSQL可以配合SCENARIO來使用,所謂SCENARIO就是在不同場景下拼接不同的SQL,具體拼接什么SQL.是根據傳遞過來的參數Map來生成相應的字符串SQL.主要實現是通過freemarker來解析的(可能公司架構師覺得這里用freemarker解析生成字符串比較簡單吧,至少不需要自己去寫解析方法.....),所以這里依賴freeMarker.
來看個def表里filterSQL的例子:
<#if SCENARIO=="ZSXMCLAUSE"> YXBZ='Y' and XYBZ='Y' and ZSXM_DM=:ZSXMDM <#elseif SCENARIO== "YXBZ"> YXBZ='Y' and XYBZ='Y' <#elseif SCENARIO== "SJZSPM"> YXBZ='Y'</#if>
比如的filterSQL配置了3種場景,如果傳過來的SCENARIO是ZSXMCLAUSE那就只拼接YXBZ='Y' and XYBZ='Y' and ZSXM_DM=:ZSXMDM 字符串
如果是SCENARIO是YXBZ那就只拼接YXBZ='Y' and XYBZ='Y'...
如果是.......
稍微總結一下的話就是說:
initParam可以讓組件在第一次和后續的請求傳遞不同的參數.
filterParam是組件每次請求都會傳遞參數
initParam和filterParam配合SCENARIO可以多個miniui組件公用一條def表配置但是拼接出不同的where條件SQL.
當然initParam和filterSQL也可以不配合 SCENARIO 來使用,這種用法下就直接把filterParam或者initParam里面的變量通過JPA的Query的setParamter方法綁定到SQL中去.但是只有一種SCENARIO的時候可以把filterSQL直接放到table_name里.反正只有一種情景..所以說一般用到filterParam的時候都會有N個SCENARIO,不同組件選擇不同的SCENARIO .
這段代碼寫的比較一般...本來有個缺陷就是當傳了SCENARIO過來走filterParam和initParam路線的時候其他的key和value會被忽略,導致部分綁定的參數沒有被設值.
所以后來修改了代碼,就是增加了各種remove和addAll什么的調用...然后在getCodeListParamMap方法里再次判斷param Map是否非空.....顯得代碼邏輯很混亂..當然....能用就行了....
有了拼接的SQL,就只要調用JPA去執行原生SQL就可以了.
小結
公司的basecode功能展示了不通過JPA映射,通過配置來實現不同表通過統一規則查數據的一種方式.還是比較方便的.
除此之外basecode還有些其他的功能,比如通過name找code,通過code找name等等.....都是通過def表來實現的...不過其他的功能都是比較簡單的,即使不通過這套代碼也可以很方便的實現...所以我就不再仔細介紹了..