主題
公司在DAO層使用的框架是Spring Data JPA,這個框架很好用,基本不需要自己寫SQL或者HQL就能完成大部分事情,但是偶爾有一些復雜的查詢還是需要自己手寫原生的Native SQL或者HQL.同時公司前端界面使用的是jquery miniui框架,並且自己進行了一些封裝.
當界面上查詢條件比較多的時候,需要動態拼接查詢條件,使用JPA的話可以通過CriteriaQuery進行面向對象的方式進行查詢,但是偶爾有時候又要用到HQL或者SQL,畢竟比CriteriaQuery簡單很多.而兩者似乎不能很好的結合(我還沒見過將兩者混用的,不過可能是我CriteriaQuery使用的比較少的原因).
這也是我寫這篇文章的原因,記錄分享一下公司的解決辦法(公司的策略也不是完美的,只能適用於簡單的查詢,復雜的情況還是不支持的,但是也不失為一種解決辦法)
原理
公司動態查詢的原理是使用HQL或者SQL,在這個基本select語句上動態拼接where條件...公司認為用戶在界面上動態選擇條件的時候,select 的表基本是不會變化的,有變化的是where字句里的條件.所以需要對用戶的輸入和where子句里的條件進行封裝.
DefaultPage:這個類是公司對前端界面用戶輸入的查詢條件,比如分頁信息等等進行的封裝.(當然還有其他很多類..只是這個是最主要的,怎么封裝的不是這篇文章的主題)
SearchCriteria:這個是公司對查詢條件where子句的封裝,它可以通過一些方法轉化成where子句里SQL字符串..
最主要的就是以上2個類,核心思想就是封裝用戶輸入的查詢信息到DefaultPage,從DefaultPage中取得SearchCriteria,將SearchCriteria轉化成字符串形式,拼接在基礎的SQL(HQL)之上,形成動態的SQL來查詢數據.
主要實現
SearchCriteria
SearchCriteria是對where子句的封裝,SearchCriteria中包含很多個小的SubCriteria,SubCriteria是對where子句里每個條件的封裝.
比如有個where子句是:
where 1=1 and user.username = 'user1' and user.state in ('0','1');
那整個where語句是1個SearchCriteria
and user.username = 'user1'是第一個SubCriteria
and user.state in ('0','1')是第二個SubCriteria
1=1是為了拼接方便,就算沒有查詢條件也拼接where條件而增加的一個拼接默認條件
1 public SubCriteria(String attName, EOperator operator, Object value, ERelation relation, String tabelAlias) { 2 this.attName = attName; 3 this.operator = operator; 4 this.attValue = value; 5 this.relation = relation; 6 this.tableAliasName = tabelAlias; 7 }
從SubCriteria的構造方法中可以看出,如果一個SubCriteria對應的是and user.username = 'user1'
那attName就是username
operator就是=
value就是'user1'
relation就是and
tableAlias就是user
1個SearchCriteria中肯定會含有N個SubCriteria
List<SubCriteria> list = new ArrayList<SubCriteria>();
查詢的數據庫結果的方法如下:
1 @Override 2 public <D> List<D> executeDynamicQuerySql(String sql, SearchCriteria criteria, Class<? extends D> targetClass, 3 Map<String, Object> customParams) { 4 Map<String, Object> paramMap = new HashMap<String, Object>(); 5 String handledSql = calculateDynamicSql(sql, criteria, paramMap); 6 mergeCustomParams(paramMap, customParams); 7 return NativeSqlExecutor.executeQuerySql(getEntityManager(), handledSql, paramMap, targetClass); 8 }
sql的格式就是類似於 select * from user user %Where_Clause% and user.yxbz = :yxbz (yxbz是有效標志的意思)
criteria就是界面上動態選擇的查詢條件和值的封裝
targetClass不重要,只是為了把結果封裝成對象時候,指定要封裝到哪個類型的對象里.(數據庫返回結果封裝到對象也是公司自己封裝的代碼)
customParams 里存是sql里一些占位符參數的鍵值對,比如key=yxbz,value='Y'
第5行代碼calculateDynamicSql將Criteria轉化成的SQL拼接到傳入的sql中,替換掉%Where_Clause%,在把Criteria中的占位符鍵值對放到paramMap中
第6行,將paramMap與傳入的customParams合並,得到一個合並的map,即是把customParams的鍵值對放到paramMap中.
第7行就是常規的調用JPA的方法,把生成的動態的SQL與參數Map傳給JPA去執行,根據targetClass將返回的結果封裝成對象.
calcilateDynamicSql具體步驟如下:
private String calculateDynamicSql(String sql, SearchCriteria criteria, Map<String, Object> paramMap) { String searchSql = calculateSearchDynamicSql(sql, criteria, paramMap); return calculateOrderDynamicSql(searchSql, criteria); } private String calculateSearchDynamicSql(String sql, SearchCriteria criteria, Map<String, Object> paramMap) { StringBuilder whereClause = new StringBuilder(" WHERE 1=1 "); int index = paramMap.keySet().size() + 1; for (SubCriteria subCriteria : criteria.getCreteriaList()) { whereClause.append(subCriteria.getRelation().getCode()); whereClause.append(StringUtils.isEmpty(subCriteria.getTableAliasName()) ? "" : subCriteria .getTableAliasName() + "."); whereClause.append(subCriteria.getAttName()); whereClause.append(" "); whereClause.append(subCriteria.getOperator().getCode()); whereClause.append(" "); if (EOperator.IN == subCriteria.getOperator()) { @SuppressWarnings("unchecked") List<Object> paramValues = (List<Object>) subCriteria.getAttValue(); whereClause.append("("); for (int i = 0; i < paramValues.size(); i++) { whereClause.append(PLACEHOLDER); String key = PARAM_PREFIX + (index++); whereClause.append(key); if (i != paramValues.size() - 1) { whereClause.append(","); } paramMap.put(key, paramValues.get(i)); } whereClause.append(")"); } else { whereClause.append(PLACEHOLDER); String key = PARAM_PREFIX + (index++); whereClause.append(key); whereClause.append(" "); paramMap.put(key, subCriteria.getAttValue()); } } return sql.replace("%WHERE_CLAUSE%", whereClause.toString()); }
calculateDynamicSql中又分為2個步驟,先根據SearchCriteria計算出where字符串,再根據SearchCriteria計算order by子句...order by子句比where子句簡單很多,原理也差不多...就不介紹了..主要看calculateSearchDynamicSql這個計算where子句的方法步驟主要是:
1.先拼接where 1=1 這是為了簡化問題,防止用戶什么都不選的時候不用拼接where子句的問題,就算動態SQL里什么where條件都不寫,也會拼接where 1=1 這個條件來簡化問題.
2.有了步驟1可以保證一定拼接了where字符串,后續只要把SubCriteria轉化成字符串拼接到where子句中就OK了. 拼接方法如前面介紹SubCriteria所說,就是把SubCriteria中的屬性一個一個取出來拼接.唯一有點區別的就是如果SubCriteria中EOperator是in操作符,那傳過來的參數值是個list而不是一個String...
mergeCustomParams(paramMap, customParams)方法
拼接完了SQL,就需要把SearchCriteria中的占位符鍵值對與用戶傳入的鍵值對合並.
1 private void mergeCustomParams(Map<String, Object> paramMap, Map<String, Object> customParams) { 2 if (null == customParams || null == paramMap) { 3 return; 4 } 5 for (Map.Entry<String, Object> entry : customParams.entrySet()) { 6 if (null != entry.getKey()) { 7 if (paramMap.containsKey(entry.getKey())) { 8 throw new IllegalArgumentException("動態SQL不允許自定義占位符以 'P_' 開始,該類占位符用於 searchCriteria動態生成。"); 9 } 10 paramMap.put(entry.getKey(), entry.getValue()); 11 } 12 } 13 }
當然其中鍵值對可能會有同名的...那就報錯...
executeQuerySql方法
1 public static <D> List<D> executeQuerySql(EntityManager entityManager, String sql, Map<String, Object> paramMap, 2 Class<? extends D> targetClass) { 3 4 LOGGER.debug("Execute native query sql {} with parameters {} for target {}", sql, paramMap, targetClass); 5 Query query = entityManager.createNativeQuery(sql); 6 if (DbColumnMapper.isNamedMapping(targetClass)) { 7 query.unwrap(SQLQuery.class).setResultTransformer(Transformers.ALIAS_TO_ENTITY_MAP); 8 } 9 if (paramMap != null) { 10 for (Entry<String, Object> entry : paramMap.entrySet()) { 11 query.setParameter(entry.getKey(), entry.getValue()); 12 } 13 } 14 return DbColumnMapper.resultMapping(query.getResultList(), targetClass); 15 }
核心就是:
1.query.setParameter(entry.getKey(), entry.getValue());根據傳入的鍵值對設置到占位符中
2.query.getResultList()查詢結果..其他很多代碼是用於把query.getResultList()的結果封裝成對象用的..主要方法是在targetClass的類的Field上使用注解標注.
DefaultPage
公司對前后台請求與參數都進行了封裝,前面寫過一篇文章,介紹了公司的基本思路(當然實現肯定不一樣)
http://www.cnblogs.com/abcwt112/p/5169250.html
所以這里不再詳細介紹前台用戶選擇的那些查詢條件怎么封裝到DefaultPage里了...
我們來看看DefaultPage如何生成SearchCriteria的:
1 private SearchCriteria buildSearchCriteria(String buildSearchType) { 2 SearchCriteria search = new SearchCriteria(); 3 operationMap = this.getOperationMap(); 4 try { 5 for (Map.Entry<String, Object> entry : parameterMap.entrySet()) { 6 String paramKey = entry.getKey(); 7 Object value = parameterMap.get(paramKey); 8 boolean isString = value instanceof String; 9 10 if (value != null) { 11 if (isString) { 12 if (StringUtils.isNotEmpty((String) value)) { 13 search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey), 14 operationMap.get(paramKey)); 15 this.parameterValues.add(value); 16 } 17 } else { 18 search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey), 19 operationMap.get(paramKey)); 20 this.parameterValues.add(value); 21 } 22 23 } 24 } 25 for (SearchOrder order : orderBy) { 26 if (StringUtils.isNotEmpty(order.getOrderName())) { 27 SearchOrder realOrder = new SearchOrder(this.getColumnName(order.getOrderName(), buildSearchType), 28 order.isAsc()); 29 search.getOrderByList().add(realOrder); 30 } 31 } 32 33 } catch (Exception ex) { 34 throw new SystemException(SystemException.REQUEST_EXCEPTION, ex, ex.getMessage()); 35 } 36 return search; 37 }
buildSearchType有2種,1種最常用的就是生成我們這里一般的SQL查詢的searchCriteria,還有一種適用於SpringDataJpa,用於生成適用於Specification接口的SearchCriteria用的...
生成SearchCriteria主要是為了生成SubCriteria,通過調用SearchCriteria的add方法直接在生成一個SubCriteria並放到SearchCriteria的List<SubCriteria>成員域中.
1 search.add(this.getColumnName(paramKey, buildSearchType), this.parameterMap.get(paramKey), 2 operationMap.get(paramKey));
1 public void add(String attribute, Object value, EOperator operator) { 2 if (attribute == null || operator == null) { 3 return; 4 } 5 list.add(new SubCriteria(attribute, operator, value)); 6 }
search.add的第一個參數是attribute就是where user.username = :username中的username
getColumn方法如下:
1 private String getColumnName(String paramKey, String buildType) throws Exception {// NOSONAR 2 if (paramKey.indexOf(':') > 0) { 3 String[] keys = paramKey.split(":"); 4 if (null != keys && keys.length == 2 && cmpClass != null) { 5 Field field = cmpClass.getDeclaredField(keys[0]); 6 if (null != field) { 7 StringBuilder sb = new StringBuilder(); 8 sb.append(keys[0]); 9 sb.append('.'); 10 if (BUILDTYPE_NATIVE.equalsIgnoreCase(buildType)) { 11 sb.append(getColumnName(keys[1], buildType, field.getClass())); 12 } else { 13 sb.append(keys[1]); 14 } 15 return sb.toString(); 16 } 17 } 18 } 19 return getColumnName(paramKey, buildType, cmpClass); 20 }
大多數情況下是直接調用19行的getColumnName
1 private String getColumnName(String paramKey, String buildType, Class<?> clazz) throws Exception {// NOSONAR 2 String columnName = paramKey; 3 if (paramKey.endsWith("_start")) { 4 columnName = paramKey.replaceAll("_start", ""); 5 } 6 if (paramKey.endsWith("_end")) { 7 columnName = paramKey.replaceAll("_end", ""); 8 } 9 if (clazz != null && BUILDTYPE_NATIVE.equalsIgnoreCase(buildType)) { 10 Field field = clazz.getDeclaredField(columnName); 11 Column column = field.getAnnotation(Column.class); 12 if (column != null) { 13 columnName = column.name(); 14 } 15 } 16 return columnName; 17 }
clazz是前台數據傳過來肯定會封裝到一個接受對象上,如果那個對象的field上用了注解@Column,那就取注解里寫的name作為attribute的name,否則就取前台傳過來的參數值作為attribute.
這是因為jpa的注解@column可以讓實體類里的字段映射到數據庫中表的字段,但是兩者的名字可以不同,將SearchCriteria轉化成SQL的時候用要使用數據庫中的字段名而不是實體類中的屬性名.
這樣就能構造出一個SearchCriteria了.
以上便是公司對DAO層動態SQL的主要封裝邏輯..在查詢條件不復雜的情況下還算好用...
但是查詢條件比較復雜的話就有點力不從心了..因為SearchCriteria里只有一個SubCriteria的list,而SubCriteria中不能包含SubCriteria...所以像 where (a.b = '1' or a.c = '2') and ....
這樣的查詢就做不出來...